base creada
This commit is contained in:
257
.github/copilot-instructions.md
vendored
Normal file
257
.github/copilot-instructions.md
vendored
Normal 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
|
||||
78
assets/referencia/tablas_energymedia.txt
Normal file
78
assets/referencia/tablas_energymedia.txt
Normal 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
|
||||
@@ -14,7 +14,7 @@ final GlobalKey<ScaffoldMessengerState> snackbarKey =
|
||||
const storage = FlutterSecureStorage();
|
||||
|
||||
final supabase = Supabase.instance.client;
|
||||
late SupabaseClient supabaseLU;
|
||||
late SupabaseClient supabaseML;
|
||||
|
||||
late final SharedPreferences prefs;
|
||||
late Configuration? theme;
|
||||
|
||||
@@ -10,9 +10,7 @@ import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:nethive_neo/providers/user_provider.dart';
|
||||
import 'package:nethive_neo/providers/visual_state_provider.dart';
|
||||
import 'package:nethive_neo/providers/users_provider.dart';
|
||||
import 'package:nethive_neo/providers/nethive/empresas_negocios_provider.dart';
|
||||
import 'package:nethive_neo/providers/nethive/componentes_provider.dart';
|
||||
import 'package:nethive_neo/providers/nethive/navigation_provider.dart';
|
||||
import 'package:nethive_neo/providers/videos_provider.dart';
|
||||
import 'package:nethive_neo/helpers/globals.dart';
|
||||
import 'package:url_strategy/url_strategy.dart';
|
||||
|
||||
@@ -32,7 +30,7 @@ void main() async {
|
||||
),
|
||||
);
|
||||
|
||||
supabaseLU = SupabaseClient(supabaseUrl, anonKey, schema: 'nethive');
|
||||
supabaseML = SupabaseClient(supabaseUrl, anonKey, schema: 'media_library');
|
||||
|
||||
await initGlobals();
|
||||
|
||||
@@ -45,9 +43,7 @@ void main() async {
|
||||
ChangeNotifierProvider(
|
||||
create: (context) => VisualStateProvider(context)),
|
||||
ChangeNotifierProvider(create: (_) => UsersProvider()),
|
||||
ChangeNotifierProvider(create: (_) => EmpresasNegociosProvider()),
|
||||
ChangeNotifierProvider(create: (_) => ComponentesProvider()),
|
||||
ChangeNotifierProvider(create: (_) => NavigationProvider()),
|
||||
ChangeNotifierProvider(create: (_) => VideosProvider()),
|
||||
],
|
||||
child: const MyApp(),
|
||||
),
|
||||
@@ -79,7 +75,7 @@ class _MyAppState extends State<MyApp> {
|
||||
Widget build(BuildContext context) {
|
||||
return Portal(
|
||||
child: MaterialApp.router(
|
||||
title: 'NETHIVE',
|
||||
title: 'EnergyMedia Content Manager',
|
||||
debugShowCheckedModeBanner: false,
|
||||
locale: _locale,
|
||||
localizationsDelegates: const [
|
||||
|
||||
58
lib/models/media/media_category_model.dart
Normal file
58
lib/models/media/media_category_model.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
156
lib/models/media/media_file_model.dart
Normal file
156
lib/models/media/media_file_model.dart
Normal 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;
|
||||
}
|
||||
5
lib/models/media/media_models.dart
Normal file
5
lib/models/media/media_models.dart
Normal 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';
|
||||
47
lib/models/media/media_poster_model.dart
Normal file
47
lib/models/media/media_poster_model.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
113
lib/models/media/media_with_poster_model.dart
Normal file
113
lib/models/media/media_with_poster_model.dart
Normal 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;
|
||||
}
|
||||
@@ -1,7 +1,3 @@
|
||||
import 'package:nethive_neo/models/nethive/componente_model.dart';
|
||||
import 'package:nethive_neo/models/nethive/conexion_componente_model.dart';
|
||||
import 'package:nethive_neo/models/nethive/conexion_alimentacion_model.dart';
|
||||
|
||||
class TopologiaCompleta {
|
||||
final List<ComponenteTopologia> componentes;
|
||||
final List<ConexionDatos> conexionesDatos;
|
||||
|
||||
@@ -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
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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 ? 'Sí' : 'No'),
|
||||
if (componente.ubicacion != null &&
|
||||
componente.ubicacion!.isNotEmpty)
|
||||
_buildDetailRow('Ubicación', componente.ubicacion!),
|
||||
if (componente.descripcion != null &&
|
||||
componente.descripcion!.isNotEmpty)
|
||||
_buildDetailRow('Descripción', componente.descripcion!),
|
||||
_buildDetailRow(
|
||||
'Fecha de Registro',
|
||||
componente.fechaRegistro?.toString().split(' ')[0] ??
|
||||
'No disponible'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDetailRow(String label, String value) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.of(context).secondaryBackground,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: AppTheme.of(context).primaryColor.withOpacity(0.2),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: AppTheme.of(context).primaryColor,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
color: AppTheme.of(context).primaryText,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _confirmDelete(dynamic componente) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
backgroundColor: AppTheme.of(context).primaryBackground,
|
||||
title: const Text('Eliminar Componente'),
|
||||
content: Text('¿Deseas eliminar "${componente.nombre}"?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(
|
||||
'Cancelar',
|
||||
style: TextStyle(color: AppTheme.of(context).secondaryText),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Eliminar próximamente')),
|
||||
);
|
||||
},
|
||||
child: const Text('Eliminar', style: TextStyle(color: Colors.red)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showEditComponenteDialog(dynamic componente) {
|
||||
final provider = Provider.of<ComponentesProvider>(context, listen: false);
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => EditComponenteDialog(
|
||||
provider: provider,
|
||||
componente: componente,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
536
lib/pages/videos/dashboard_page.dart
Normal file
536
lib/pages/videos/dashboard_page.dart
Normal 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';
|
||||
}
|
||||
}
|
||||
}
|
||||
823
lib/pages/videos/gestor_videos_page.dart
Normal file
823
lib/pages/videos/gestor_videos_page.dart
Normal 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';
|
||||
}
|
||||
}
|
||||
}
|
||||
1109
lib/pages/videos/premium_dashboard_page.dart
Normal file
1109
lib/pages/videos/premium_dashboard_page.dart
Normal file
File diff suppressed because it is too large
Load Diff
560
lib/pages/videos/videos_layout.dart
Normal file
560
lib/pages/videos/videos_layout.dart
Normal 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,
|
||||
});
|
||||
}
|
||||
634
lib/pages/videos/widgets/premium_upload_dialog.dart
Normal file
634
lib/pages/videos/widgets/premium_upload_dialog.dart
Normal 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
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -1,6 +1,3 @@
|
||||
export 'package:nethive_neo/providers/visual_state_provider.dart';
|
||||
export 'package:nethive_neo/providers/users_provider.dart';
|
||||
export 'package:nethive_neo/providers/user_provider.dart';
|
||||
export 'package:nethive_neo/providers/nethive/empresas_negocios_provider.dart';
|
||||
export 'package:nethive_neo/providers/nethive/componentes_provider.dart';
|
||||
export 'package:nethive_neo/providers/nethive/navigation_provider.dart';
|
||||
|
||||
690
lib/providers/videos_provider.dart
Normal file
690
lib/providers/videos_provider.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -167,6 +167,12 @@ class VisualStateProvider extends ChangeNotifier {
|
||||
isTaped[index] = true;
|
||||
}
|
||||
|
||||
void changeThemeMode(ThemeMode mode, BuildContext context) {
|
||||
AppTheme.saveThemeMode(mode);
|
||||
setDarkModeSetting(context, mode);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
primaryColorLightController.dispose();
|
||||
|
||||
@@ -2,8 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import 'package:nethive_neo/helpers/globals.dart';
|
||||
import 'package:nethive_neo/pages/empresa_negocios/empresa_negocios_page.dart';
|
||||
import 'package:nethive_neo/pages/infrastructure/infrastructure_layout.dart';
|
||||
import 'package:nethive_neo/pages/videos/videos_layout.dart';
|
||||
import 'package:nethive_neo/pages/pages.dart';
|
||||
import 'package:nethive_neo/services/navigation_service.dart';
|
||||
|
||||
@@ -36,23 +35,7 @@ final GoRouter router = GoRouter(
|
||||
path: '/',
|
||||
name: 'root',
|
||||
builder: (BuildContext context, GoRouterState state) {
|
||||
if (currentUser!.role.roleId == 14 || currentUser!.role.roleId == 13) {
|
||||
return Container(
|
||||
color: Colors.amber,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
height: MediaQuery.of(context).size.height,
|
||||
child: const Center(child: Text('Empresa Negocios')));
|
||||
} else {
|
||||
return const EmpresaNegociosPage();
|
||||
}
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: '/infrastructure/:negocioId',
|
||||
name: 'infrastructure',
|
||||
builder: (BuildContext context, GoRouterState state) {
|
||||
final negocioId = state.pathParameters['negocioId']!;
|
||||
return InfrastructureLayout(negocioId: negocioId);
|
||||
return const VideosLayout();
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
|
||||
@@ -76,13 +76,11 @@ abstract class AppTheme {
|
||||
);
|
||||
|
||||
Gradient primaryGradient = const LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
begin: Alignment(-1.0, -1.0),
|
||||
end: Alignment(1.0, 1.0),
|
||||
colors: <Color>[
|
||||
Color(0xFF10B981), // Verde esmeralda
|
||||
Color(0xFF059669), // Verde intenso
|
||||
Color(0xFF0D9488), // Verde-azulado
|
||||
Color(0xFF0F172A), // Azul muy oscuro
|
||||
Color(0xFF4EC9F5), // Cyan
|
||||
Color(0xFFFFB733), // Yellow
|
||||
],
|
||||
);
|
||||
|
||||
@@ -91,10 +89,9 @@ abstract class AppTheme {
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: <Color>[
|
||||
Color(0xFF1E40AF), // Azul profundo
|
||||
Color(0xFF3B82F6), // Azul brillante
|
||||
Color(0xFF10B981), // Verde esmeralda
|
||||
Color(0xFF7C3AED), // Púrpura
|
||||
Color(0xFF6B2F8A), // Purple
|
||||
Color(0xFF4EC9F5), // Cyan
|
||||
Color(0xFFFFB733), // Yellow
|
||||
],
|
||||
);
|
||||
|
||||
@@ -103,9 +100,9 @@ abstract class AppTheme {
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: <Color>[
|
||||
Color(0xFF0F172A), // Azul muy oscuro
|
||||
Color(0xFF1E293B), // Azul oscuro
|
||||
Color(0xFF334155), // Azul gris
|
||||
Color(0xFF0B0B0D), // Main background
|
||||
Color(0xFF121214), // Surface 1
|
||||
Color(0xFF1A1A1D), // Surface 2
|
||||
],
|
||||
);
|
||||
|
||||
@@ -135,37 +132,36 @@ abstract class AppTheme {
|
||||
|
||||
class LightModeTheme extends AppTheme {
|
||||
@override
|
||||
Color primaryColor = const Color(0xFF10B981); // Verde esmeralda principal
|
||||
Color primaryColor = const Color(0xFF4EC9F5); // Cyan primary
|
||||
@override
|
||||
Color secondaryColor = const Color(0xFF059669); // Verde más oscuro
|
||||
Color secondaryColor = const Color(0xFFFFB733); // Yellow secondary
|
||||
@override
|
||||
Color tertiaryColor = const Color(0xFF0D9488); // Verde azulado
|
||||
Color tertiaryColor = const Color(0xFF6B2F8A); // Purple accent
|
||||
@override
|
||||
Color alternate = const Color(0xFF3B82F6); // Azul de acento
|
||||
Color alternate = const Color(0xFFFF7A3D); // Orange accent
|
||||
@override
|
||||
Color primaryBackground = const Color(0xFF0F172A); // Fondo muy oscuro
|
||||
Color primaryBackground = const Color(0xFFFFFFFF); // Main background (white)
|
||||
@override
|
||||
Color secondaryBackground =
|
||||
const Color(0xFF1E293B); // Fondo secundario oscuro
|
||||
Color secondaryBackground = const Color(0xFFF7F7F7); // Card/Surface 1
|
||||
@override
|
||||
Color tertiaryBackground = const Color(0xFF334155); // Fondo terciario
|
||||
Color tertiaryBackground = const Color(0xFFEFEFEF); // Card/Surface 2
|
||||
@override
|
||||
Color transparentBackground =
|
||||
const Color(0xFF1E293B).withOpacity(.1); // Fondo transparente
|
||||
const Color(0xFFFFFFFF).withOpacity(.1); // Transparent background
|
||||
@override
|
||||
Color primaryText = const Color(0xFFFFFFFF); // Texto blanco
|
||||
Color primaryText = const Color(0xFF111111); // Primary text
|
||||
@override
|
||||
Color secondaryText = const Color(0xFF94A3B8); // Texto gris claro
|
||||
Color secondaryText = const Color(0xFF2B2B2B); // Secondary text
|
||||
@override
|
||||
Color tertiaryText = const Color(0xFF64748B); // Texto gris medio
|
||||
Color tertiaryText = const Color(0xFFB5B7BA); // Disabled/Muted
|
||||
@override
|
||||
Color hintText = const Color(0xFF475569); // Texto de sugerencia
|
||||
Color hintText = const Color(0xFFE1E1E1); // Border/Divider
|
||||
@override
|
||||
Color error = const Color(0xFFEF4444); // Rojo para errores
|
||||
Color error = const Color(0xFFFF2D2D); // Red accent
|
||||
@override
|
||||
Color warning = const Color(0xFFF59E0B); // Amarillo para advertencias
|
||||
Color warning = const Color(0xFFFFB733); // Yellow accent
|
||||
@override
|
||||
Color success = const Color(0xFF10B981); // Verde para éxito
|
||||
Color success = const Color(0xFF4EC9F5); // Cyan accent
|
||||
@override
|
||||
Color formBackground =
|
||||
const Color(0xFF10B981).withOpacity(.05); // Fondo de formularios
|
||||
@@ -183,37 +179,36 @@ class LightModeTheme extends AppTheme {
|
||||
|
||||
class DarkModeTheme extends AppTheme {
|
||||
@override
|
||||
Color primaryColor = const Color(0xFF10B981); // Verde esmeralda principal
|
||||
Color primaryColor = const Color(0xFF4EC9F5); // Cyan primary
|
||||
@override
|
||||
Color secondaryColor = const Color(0xFF059669); // Verde más oscuro
|
||||
Color secondaryColor = const Color(0xFFFFB733); // Yellow secondary
|
||||
@override
|
||||
Color tertiaryColor = const Color(0xFF0D9488); // Verde azulado
|
||||
Color tertiaryColor = const Color(0xFF6B2F8A); // Purple accent
|
||||
@override
|
||||
Color alternate = const Color(0xFF3B82F6); // Azul de acento
|
||||
Color alternate = const Color(0xFFFF7A3D); // Orange accent
|
||||
@override
|
||||
Color primaryBackground = const Color(0xFF0F172A); // Fondo muy oscuro
|
||||
Color primaryBackground = const Color(0xFF0B0B0D); // Main background (dark)
|
||||
@override
|
||||
Color secondaryBackground =
|
||||
const Color(0xFF1E293B); // Fondo secundario oscuro
|
||||
Color secondaryBackground = const Color(0xFF121214); // Card/Surface 1
|
||||
@override
|
||||
Color tertiaryBackground = const Color(0xFF334155); // Fondo terciario
|
||||
Color tertiaryBackground = const Color(0xFF1A1A1D); // Card/Surface 2
|
||||
@override
|
||||
Color transparentBackground =
|
||||
const Color(0xFF1E293B).withOpacity(.3); // Fondo transparente
|
||||
const Color(0xFF0B0B0D).withOpacity(.3); // Transparent background
|
||||
@override
|
||||
Color primaryText = const Color(0xFFFFFFFF); // Texto blanco
|
||||
Color primaryText = const Color(0xFFFFFFFF); // Primary text (white)
|
||||
@override
|
||||
Color secondaryText = const Color(0xFF94A3B8); // Texto gris claro
|
||||
Color secondaryText = const Color(0xFFEAEAEA); // Secondary text
|
||||
@override
|
||||
Color tertiaryText = const Color(0xFF64748B); // Texto gris medio
|
||||
Color tertiaryText = const Color(0xFF6D6E73); // Muted/Disabled
|
||||
@override
|
||||
Color hintText = const Color(0xFF475569); // Texto de sugerencia
|
||||
Color hintText = const Color(0xFF2A2A2E); // Border/Divider
|
||||
@override
|
||||
Color error = const Color(0xFFEF4444); // Rojo para errores
|
||||
Color error = const Color(0xFFFF2D2D); // Red accent
|
||||
@override
|
||||
Color warning = const Color(0xFFF59E0B); // Amarillo para advertencias
|
||||
Color warning = const Color(0xFFFFB733); // Yellow accent
|
||||
@override
|
||||
Color success = const Color(0xFF10B981); // Verde para éxito
|
||||
Color success = const Color(0xFF4EC9F5); // Cyan accent
|
||||
@override
|
||||
Color formBackground =
|
||||
const Color(0xFF10B981).withOpacity(.1); // Fondo de formularios
|
||||
|
||||
156
lib/widgets/premium_button.dart
Normal file
156
lib/widgets/premium_button.dart
Normal 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',
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import path_provider_foundation
|
||||
import shared_preferences_foundation
|
||||
import sign_in_with_apple
|
||||
import url_launcher_macos
|
||||
import wakelock_macos
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
|
||||
@@ -25,4 +26,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
SignInWithApplePlugin.register(with: registry.registrar(forPlugin: "SignInWithApplePlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
WakelockMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockMacosPlugin"))
|
||||
}
|
||||
|
||||
116
pubspec.lock
116
pubspec.lock
@@ -97,6 +97,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
chewie:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: chewie
|
||||
sha256: "3bc4fbae99a9f39dc9c04ba70ad2a2fb902dbd3d3e36ad8ab3a53f0dcbf311be"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
chip_list:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -153,6 +161,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
csslib:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: csslib
|
||||
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -229,10 +245,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: equatable
|
||||
sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7"
|
||||
sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.7"
|
||||
version: "2.0.8"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -309,10 +325,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: fl_chart
|
||||
sha256: "74959b99b92b9eebeed1a4049426fd67c4abc3c5a0f4d12e2877097d6a11ae08"
|
||||
sha256: "94307bef3a324a0d329d3ab77b2f0c6e5ed739185ffc029ed28c0f9b019ea7ef"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.69.2"
|
||||
version: "0.69.0"
|
||||
fl_heatmap:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -589,6 +605,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
html:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: html
|
||||
sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.15.6"
|
||||
http:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1085,6 +1109,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
shimmer:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: shimmer
|
||||
sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
sign_in_with_apple:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1314,6 +1346,46 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
video_player:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: video_player
|
||||
sha256: "868a139229acb5018d22aded3eb9cb4767ff43a8216573c086b6c535a4957481"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.6.0"
|
||||
video_player_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: video_player_android
|
||||
sha256: e7de6fabe5d96048cd8f4d710f25c3df84bb3cab8b22da6c082bd8f39e316984
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.3"
|
||||
video_player_avfoundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: video_player_avfoundation
|
||||
sha256: "90468226c8687adf7b567d9bb42c25588783c4d30509af1fbd663b2dd049f700"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
video_player_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: video_player_platform_interface
|
||||
sha256: "57c5d73173f76d801129d0531c2774052c5a7c11ccb962f1830630decd9f24ec"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.6.0"
|
||||
video_player_web:
|
||||
dependency: "direct overridden"
|
||||
description:
|
||||
name: video_player_web
|
||||
sha256: "9f3c00be2ef9b76a95d94ac5119fb843dca6f2c69e6c9968f6f2b6c9e7afbdeb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1322,6 +1394,38 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "15.0.0"
|
||||
wakelock:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: wakelock
|
||||
sha256: "78bad4822be81d37e7bc34b6990da7dface2a445255cd37c6f053b51a4ccdb3b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.0"
|
||||
wakelock_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: wakelock_macos
|
||||
sha256: "73581e5d9ed2dd1ba951375c30e63f0eb8c58d7d6286ae9ddf927b88f2aea8d9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.0+3"
|
||||
wakelock_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: wakelock_platform_interface
|
||||
sha256: d0a8a1c02af68077db5df1e0f5e2b745f7b1f2cdcc48e3e0b6f8f4dcc349050e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.1+3"
|
||||
wakelock_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: wakelock_web
|
||||
sha256: "06b0033d5421712138e7fa482ff5c6280fe12e0a41c40c3fe8fda2c007eb4348"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0+3"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1403,5 +1507,5 @@ packages:
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
sdks:
|
||||
dart: ">=3.7.0-0 <4.0.0"
|
||||
flutter: ">=3.22.0"
|
||||
dart: ">=3.7.0 <4.0.0"
|
||||
flutter: ">=3.29.0"
|
||||
|
||||
@@ -53,7 +53,6 @@ dependencies:
|
||||
countries_world_map: ^1.1.1
|
||||
customizable_counter: ^1.0.4
|
||||
drop_down_list_menu: ^0.0.9
|
||||
fl_chart: ^0.69.0
|
||||
fl_heatmap: ^0.4.4
|
||||
flip_card: ^0.7.0
|
||||
flutter_credit_card: ^4.0.1
|
||||
@@ -74,6 +73,10 @@ dependencies:
|
||||
flutter_graph_view: ^1.1.6
|
||||
flutter_animate: ^4.2.0
|
||||
flutter_flow_chart: 3.2.3
|
||||
video_player: ^2.6.0
|
||||
chewie: ^1.0.0
|
||||
shimmer: ^3.0.0
|
||||
fl_chart: 0.69.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
@@ -83,6 +86,7 @@ dev_dependencies:
|
||||
|
||||
dependency_overrides:
|
||||
intl: ^0.19.0 # Override para que pluto_grid funcione
|
||||
video_player_web: ^2.3.0 # Override para compatibilidad con Flutter web
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
|
||||
Reference in New Issue
Block a user