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();
|
const storage = FlutterSecureStorage();
|
||||||
|
|
||||||
final supabase = Supabase.instance.client;
|
final supabase = Supabase.instance.client;
|
||||||
late SupabaseClient supabaseLU;
|
late SupabaseClient supabaseML;
|
||||||
|
|
||||||
late final SharedPreferences prefs;
|
late final SharedPreferences prefs;
|
||||||
late Configuration? theme;
|
late Configuration? theme;
|
||||||
|
|||||||
@@ -10,9 +10,7 @@ import 'package:supabase_flutter/supabase_flutter.dart';
|
|||||||
import 'package:nethive_neo/providers/user_provider.dart';
|
import 'package:nethive_neo/providers/user_provider.dart';
|
||||||
import 'package:nethive_neo/providers/visual_state_provider.dart';
|
import 'package:nethive_neo/providers/visual_state_provider.dart';
|
||||||
import 'package:nethive_neo/providers/users_provider.dart';
|
import 'package:nethive_neo/providers/users_provider.dart';
|
||||||
import 'package:nethive_neo/providers/nethive/empresas_negocios_provider.dart';
|
import 'package:nethive_neo/providers/videos_provider.dart';
|
||||||
import 'package:nethive_neo/providers/nethive/componentes_provider.dart';
|
|
||||||
import 'package:nethive_neo/providers/nethive/navigation_provider.dart';
|
|
||||||
import 'package:nethive_neo/helpers/globals.dart';
|
import 'package:nethive_neo/helpers/globals.dart';
|
||||||
import 'package:url_strategy/url_strategy.dart';
|
import 'package:url_strategy/url_strategy.dart';
|
||||||
|
|
||||||
@@ -32,7 +30,7 @@ void main() async {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
supabaseLU = SupabaseClient(supabaseUrl, anonKey, schema: 'nethive');
|
supabaseML = SupabaseClient(supabaseUrl, anonKey, schema: 'media_library');
|
||||||
|
|
||||||
await initGlobals();
|
await initGlobals();
|
||||||
|
|
||||||
@@ -45,9 +43,7 @@ void main() async {
|
|||||||
ChangeNotifierProvider(
|
ChangeNotifierProvider(
|
||||||
create: (context) => VisualStateProvider(context)),
|
create: (context) => VisualStateProvider(context)),
|
||||||
ChangeNotifierProvider(create: (_) => UsersProvider()),
|
ChangeNotifierProvider(create: (_) => UsersProvider()),
|
||||||
ChangeNotifierProvider(create: (_) => EmpresasNegociosProvider()),
|
ChangeNotifierProvider(create: (_) => VideosProvider()),
|
||||||
ChangeNotifierProvider(create: (_) => ComponentesProvider()),
|
|
||||||
ChangeNotifierProvider(create: (_) => NavigationProvider()),
|
|
||||||
],
|
],
|
||||||
child: const MyApp(),
|
child: const MyApp(),
|
||||||
),
|
),
|
||||||
@@ -79,7 +75,7 @@ class _MyAppState extends State<MyApp> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Portal(
|
return Portal(
|
||||||
child: MaterialApp.router(
|
child: MaterialApp.router(
|
||||||
title: 'NETHIVE',
|
title: 'EnergyMedia Content Manager',
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
locale: _locale,
|
locale: _locale,
|
||||||
localizationsDelegates: const [
|
localizationsDelegates: const [
|
||||||
|
|||||||
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 {
|
class TopologiaCompleta {
|
||||||
final List<ComponenteTopologia> componentes;
|
final List<ComponenteTopologia> componentes;
|
||||||
final List<ConexionDatos> conexionesDatos;
|
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/visual_state_provider.dart';
|
||||||
export 'package:nethive_neo/providers/users_provider.dart';
|
export 'package:nethive_neo/providers/users_provider.dart';
|
||||||
export 'package:nethive_neo/providers/user_provider.dart';
|
export 'package:nethive_neo/providers/user_provider.dart';
|
||||||
export 'package:nethive_neo/providers/nethive/empresas_negocios_provider.dart';
|
|
||||||
export 'package:nethive_neo/providers/nethive/componentes_provider.dart';
|
|
||||||
export 'package:nethive_neo/providers/nethive/navigation_provider.dart';
|
|
||||||
|
|||||||
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;
|
isTaped[index] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void changeThemeMode(ThemeMode mode, BuildContext context) {
|
||||||
|
AppTheme.saveThemeMode(mode);
|
||||||
|
setDarkModeSetting(context, mode);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
primaryColorLightController.dispose();
|
primaryColorLightController.dispose();
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
import 'package:nethive_neo/helpers/globals.dart';
|
import 'package:nethive_neo/helpers/globals.dart';
|
||||||
import 'package:nethive_neo/pages/empresa_negocios/empresa_negocios_page.dart';
|
import 'package:nethive_neo/pages/videos/videos_layout.dart';
|
||||||
import 'package:nethive_neo/pages/infrastructure/infrastructure_layout.dart';
|
|
||||||
import 'package:nethive_neo/pages/pages.dart';
|
import 'package:nethive_neo/pages/pages.dart';
|
||||||
import 'package:nethive_neo/services/navigation_service.dart';
|
import 'package:nethive_neo/services/navigation_service.dart';
|
||||||
|
|
||||||
@@ -36,23 +35,7 @@ final GoRouter router = GoRouter(
|
|||||||
path: '/',
|
path: '/',
|
||||||
name: 'root',
|
name: 'root',
|
||||||
builder: (BuildContext context, GoRouterState state) {
|
builder: (BuildContext context, GoRouterState state) {
|
||||||
if (currentUser!.role.roleId == 14 || currentUser!.role.roleId == 13) {
|
return const VideosLayout();
|
||||||
return Container(
|
|
||||||
color: Colors.amber,
|
|
||||||
width: MediaQuery.of(context).size.width,
|
|
||||||
height: MediaQuery.of(context).size.height,
|
|
||||||
child: const Center(child: Text('Empresa Negocios')));
|
|
||||||
} else {
|
|
||||||
return const EmpresaNegociosPage();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: '/infrastructure/:negocioId',
|
|
||||||
name: 'infrastructure',
|
|
||||||
builder: (BuildContext context, GoRouterState state) {
|
|
||||||
final negocioId = state.pathParameters['negocioId']!;
|
|
||||||
return InfrastructureLayout(negocioId: negocioId);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
|
|||||||
@@ -76,13 +76,11 @@ abstract class AppTheme {
|
|||||||
);
|
);
|
||||||
|
|
||||||
Gradient primaryGradient = const LinearGradient(
|
Gradient primaryGradient = const LinearGradient(
|
||||||
begin: Alignment.topLeft,
|
begin: Alignment(-1.0, -1.0),
|
||||||
end: Alignment.bottomRight,
|
end: Alignment(1.0, 1.0),
|
||||||
colors: <Color>[
|
colors: <Color>[
|
||||||
Color(0xFF10B981), // Verde esmeralda
|
Color(0xFF4EC9F5), // Cyan
|
||||||
Color(0xFF059669), // Verde intenso
|
Color(0xFFFFB733), // Yellow
|
||||||
Color(0xFF0D9488), // Verde-azulado
|
|
||||||
Color(0xFF0F172A), // Azul muy oscuro
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -91,10 +89,9 @@ abstract class AppTheme {
|
|||||||
begin: Alignment.topLeft,
|
begin: Alignment.topLeft,
|
||||||
end: Alignment.bottomRight,
|
end: Alignment.bottomRight,
|
||||||
colors: <Color>[
|
colors: <Color>[
|
||||||
Color(0xFF1E40AF), // Azul profundo
|
Color(0xFF6B2F8A), // Purple
|
||||||
Color(0xFF3B82F6), // Azul brillante
|
Color(0xFF4EC9F5), // Cyan
|
||||||
Color(0xFF10B981), // Verde esmeralda
|
Color(0xFFFFB733), // Yellow
|
||||||
Color(0xFF7C3AED), // Púrpura
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -103,9 +100,9 @@ abstract class AppTheme {
|
|||||||
begin: Alignment.topCenter,
|
begin: Alignment.topCenter,
|
||||||
end: Alignment.bottomCenter,
|
end: Alignment.bottomCenter,
|
||||||
colors: <Color>[
|
colors: <Color>[
|
||||||
Color(0xFF0F172A), // Azul muy oscuro
|
Color(0xFF0B0B0D), // Main background
|
||||||
Color(0xFF1E293B), // Azul oscuro
|
Color(0xFF121214), // Surface 1
|
||||||
Color(0xFF334155), // Azul gris
|
Color(0xFF1A1A1D), // Surface 2
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -135,37 +132,36 @@ abstract class AppTheme {
|
|||||||
|
|
||||||
class LightModeTheme extends AppTheme {
|
class LightModeTheme extends AppTheme {
|
||||||
@override
|
@override
|
||||||
Color primaryColor = const Color(0xFF10B981); // Verde esmeralda principal
|
Color primaryColor = const Color(0xFF4EC9F5); // Cyan primary
|
||||||
@override
|
@override
|
||||||
Color secondaryColor = const Color(0xFF059669); // Verde más oscuro
|
Color secondaryColor = const Color(0xFFFFB733); // Yellow secondary
|
||||||
@override
|
@override
|
||||||
Color tertiaryColor = const Color(0xFF0D9488); // Verde azulado
|
Color tertiaryColor = const Color(0xFF6B2F8A); // Purple accent
|
||||||
@override
|
@override
|
||||||
Color alternate = const Color(0xFF3B82F6); // Azul de acento
|
Color alternate = const Color(0xFFFF7A3D); // Orange accent
|
||||||
@override
|
@override
|
||||||
Color primaryBackground = const Color(0xFF0F172A); // Fondo muy oscuro
|
Color primaryBackground = const Color(0xFFFFFFFF); // Main background (white)
|
||||||
@override
|
@override
|
||||||
Color secondaryBackground =
|
Color secondaryBackground = const Color(0xFFF7F7F7); // Card/Surface 1
|
||||||
const Color(0xFF1E293B); // Fondo secundario oscuro
|
|
||||||
@override
|
@override
|
||||||
Color tertiaryBackground = const Color(0xFF334155); // Fondo terciario
|
Color tertiaryBackground = const Color(0xFFEFEFEF); // Card/Surface 2
|
||||||
@override
|
@override
|
||||||
Color transparentBackground =
|
Color transparentBackground =
|
||||||
const Color(0xFF1E293B).withOpacity(.1); // Fondo transparente
|
const Color(0xFFFFFFFF).withOpacity(.1); // Transparent background
|
||||||
@override
|
@override
|
||||||
Color primaryText = const Color(0xFFFFFFFF); // Texto blanco
|
Color primaryText = const Color(0xFF111111); // Primary text
|
||||||
@override
|
@override
|
||||||
Color secondaryText = const Color(0xFF94A3B8); // Texto gris claro
|
Color secondaryText = const Color(0xFF2B2B2B); // Secondary text
|
||||||
@override
|
@override
|
||||||
Color tertiaryText = const Color(0xFF64748B); // Texto gris medio
|
Color tertiaryText = const Color(0xFFB5B7BA); // Disabled/Muted
|
||||||
@override
|
@override
|
||||||
Color hintText = const Color(0xFF475569); // Texto de sugerencia
|
Color hintText = const Color(0xFFE1E1E1); // Border/Divider
|
||||||
@override
|
@override
|
||||||
Color error = const Color(0xFFEF4444); // Rojo para errores
|
Color error = const Color(0xFFFF2D2D); // Red accent
|
||||||
@override
|
@override
|
||||||
Color warning = const Color(0xFFF59E0B); // Amarillo para advertencias
|
Color warning = const Color(0xFFFFB733); // Yellow accent
|
||||||
@override
|
@override
|
||||||
Color success = const Color(0xFF10B981); // Verde para éxito
|
Color success = const Color(0xFF4EC9F5); // Cyan accent
|
||||||
@override
|
@override
|
||||||
Color formBackground =
|
Color formBackground =
|
||||||
const Color(0xFF10B981).withOpacity(.05); // Fondo de formularios
|
const Color(0xFF10B981).withOpacity(.05); // Fondo de formularios
|
||||||
@@ -183,37 +179,36 @@ class LightModeTheme extends AppTheme {
|
|||||||
|
|
||||||
class DarkModeTheme extends AppTheme {
|
class DarkModeTheme extends AppTheme {
|
||||||
@override
|
@override
|
||||||
Color primaryColor = const Color(0xFF10B981); // Verde esmeralda principal
|
Color primaryColor = const Color(0xFF4EC9F5); // Cyan primary
|
||||||
@override
|
@override
|
||||||
Color secondaryColor = const Color(0xFF059669); // Verde más oscuro
|
Color secondaryColor = const Color(0xFFFFB733); // Yellow secondary
|
||||||
@override
|
@override
|
||||||
Color tertiaryColor = const Color(0xFF0D9488); // Verde azulado
|
Color tertiaryColor = const Color(0xFF6B2F8A); // Purple accent
|
||||||
@override
|
@override
|
||||||
Color alternate = const Color(0xFF3B82F6); // Azul de acento
|
Color alternate = const Color(0xFFFF7A3D); // Orange accent
|
||||||
@override
|
@override
|
||||||
Color primaryBackground = const Color(0xFF0F172A); // Fondo muy oscuro
|
Color primaryBackground = const Color(0xFF0B0B0D); // Main background (dark)
|
||||||
@override
|
@override
|
||||||
Color secondaryBackground =
|
Color secondaryBackground = const Color(0xFF121214); // Card/Surface 1
|
||||||
const Color(0xFF1E293B); // Fondo secundario oscuro
|
|
||||||
@override
|
@override
|
||||||
Color tertiaryBackground = const Color(0xFF334155); // Fondo terciario
|
Color tertiaryBackground = const Color(0xFF1A1A1D); // Card/Surface 2
|
||||||
@override
|
@override
|
||||||
Color transparentBackground =
|
Color transparentBackground =
|
||||||
const Color(0xFF1E293B).withOpacity(.3); // Fondo transparente
|
const Color(0xFF0B0B0D).withOpacity(.3); // Transparent background
|
||||||
@override
|
@override
|
||||||
Color primaryText = const Color(0xFFFFFFFF); // Texto blanco
|
Color primaryText = const Color(0xFFFFFFFF); // Primary text (white)
|
||||||
@override
|
@override
|
||||||
Color secondaryText = const Color(0xFF94A3B8); // Texto gris claro
|
Color secondaryText = const Color(0xFFEAEAEA); // Secondary text
|
||||||
@override
|
@override
|
||||||
Color tertiaryText = const Color(0xFF64748B); // Texto gris medio
|
Color tertiaryText = const Color(0xFF6D6E73); // Muted/Disabled
|
||||||
@override
|
@override
|
||||||
Color hintText = const Color(0xFF475569); // Texto de sugerencia
|
Color hintText = const Color(0xFF2A2A2E); // Border/Divider
|
||||||
@override
|
@override
|
||||||
Color error = const Color(0xFFEF4444); // Rojo para errores
|
Color error = const Color(0xFFFF2D2D); // Red accent
|
||||||
@override
|
@override
|
||||||
Color warning = const Color(0xFFF59E0B); // Amarillo para advertencias
|
Color warning = const Color(0xFFFFB733); // Yellow accent
|
||||||
@override
|
@override
|
||||||
Color success = const Color(0xFF10B981); // Verde para éxito
|
Color success = const Color(0xFF4EC9F5); // Cyan accent
|
||||||
@override
|
@override
|
||||||
Color formBackground =
|
Color formBackground =
|
||||||
const Color(0xFF10B981).withOpacity(.1); // Fondo de formularios
|
const Color(0xFF10B981).withOpacity(.1); // Fondo de formularios
|
||||||
|
|||||||
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 shared_preferences_foundation
|
||||||
import sign_in_with_apple
|
import sign_in_with_apple
|
||||||
import url_launcher_macos
|
import url_launcher_macos
|
||||||
|
import wakelock_macos
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
|
AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
|
||||||
@@ -25,4 +26,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
SignInWithApplePlugin.register(with: registry.registrar(forPlugin: "SignInWithApplePlugin"))
|
SignInWithApplePlugin.register(with: registry.registrar(forPlugin: "SignInWithApplePlugin"))
|
||||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||||
|
WakelockMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockMacosPlugin"))
|
||||||
}
|
}
|
||||||
|
|||||||
116
pubspec.lock
116
pubspec.lock
@@ -97,6 +97,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
version: "1.4.0"
|
||||||
|
chewie:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: chewie
|
||||||
|
sha256: "3bc4fbae99a9f39dc9c04ba70ad2a2fb902dbd3d3e36ad8ab3a53f0dcbf311be"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
chip_list:
|
chip_list:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -153,6 +161,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.3"
|
version: "3.0.3"
|
||||||
|
csslib:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: csslib
|
||||||
|
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.2"
|
||||||
cupertino_icons:
|
cupertino_icons:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -229,10 +245,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: equatable
|
name: equatable
|
||||||
sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7"
|
sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.7"
|
version: "2.0.8"
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -309,10 +325,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: fl_chart
|
name: fl_chart
|
||||||
sha256: "74959b99b92b9eebeed1a4049426fd67c4abc3c5a0f4d12e2877097d6a11ae08"
|
sha256: "94307bef3a324a0d329d3ab77b2f0c6e5ed739185ffc029ed28c0f9b019ea7ef"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.69.2"
|
version: "0.69.0"
|
||||||
fl_heatmap:
|
fl_heatmap:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -589,6 +605,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0"
|
version: "1.1.0"
|
||||||
|
html:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: html
|
||||||
|
sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.15.6"
|
||||||
http:
|
http:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -1085,6 +1109,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.2"
|
version: "2.3.2"
|
||||||
|
shimmer:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: shimmer
|
||||||
|
sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.0"
|
||||||
sign_in_with_apple:
|
sign_in_with_apple:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1314,6 +1346,46 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.4"
|
version: "2.1.4"
|
||||||
|
video_player:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: video_player
|
||||||
|
sha256: "868a139229acb5018d22aded3eb9cb4767ff43a8216573c086b6c535a4957481"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.6.0"
|
||||||
|
video_player_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: video_player_android
|
||||||
|
sha256: e7de6fabe5d96048cd8f4d710f25c3df84bb3cab8b22da6c082bd8f39e316984
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.3"
|
||||||
|
video_player_avfoundation:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: video_player_avfoundation
|
||||||
|
sha256: "90468226c8687adf7b567d9bb42c25588783c4d30509af1fbd663b2dd049f700"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.2"
|
||||||
|
video_player_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: video_player_platform_interface
|
||||||
|
sha256: "57c5d73173f76d801129d0531c2774052c5a7c11ccb962f1830630decd9f24ec"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.6.0"
|
||||||
|
video_player_web:
|
||||||
|
dependency: "direct overridden"
|
||||||
|
description:
|
||||||
|
name: video_player_web
|
||||||
|
sha256: "9f3c00be2ef9b76a95d94ac5119fb843dca6f2c69e6c9968f6f2b6c9e7afbdeb"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.0"
|
||||||
vm_service:
|
vm_service:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1322,6 +1394,38 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "15.0.0"
|
version: "15.0.0"
|
||||||
|
wakelock:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: wakelock
|
||||||
|
sha256: "78bad4822be81d37e7bc34b6990da7dface2a445255cd37c6f053b51a4ccdb3b"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.4.0"
|
||||||
|
wakelock_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: wakelock_macos
|
||||||
|
sha256: "73581e5d9ed2dd1ba951375c30e63f0eb8c58d7d6286ae9ddf927b88f2aea8d9"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.0+3"
|
||||||
|
wakelock_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: wakelock_platform_interface
|
||||||
|
sha256: d0a8a1c02af68077db5df1e0f5e2b745f7b1f2cdcc48e3e0b6f8f4dcc349050e
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.1+3"
|
||||||
|
wakelock_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: wakelock_web
|
||||||
|
sha256: "06b0033d5421712138e7fa482ff5c6280fe12e0a41c40c3fe8fda2c007eb4348"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.0+3"
|
||||||
web:
|
web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1403,5 +1507,5 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.1"
|
version: "1.1.1"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.7.0-0 <4.0.0"
|
dart: ">=3.7.0 <4.0.0"
|
||||||
flutter: ">=3.22.0"
|
flutter: ">=3.29.0"
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ dependencies:
|
|||||||
countries_world_map: ^1.1.1
|
countries_world_map: ^1.1.1
|
||||||
customizable_counter: ^1.0.4
|
customizable_counter: ^1.0.4
|
||||||
drop_down_list_menu: ^0.0.9
|
drop_down_list_menu: ^0.0.9
|
||||||
fl_chart: ^0.69.0
|
|
||||||
fl_heatmap: ^0.4.4
|
fl_heatmap: ^0.4.4
|
||||||
flip_card: ^0.7.0
|
flip_card: ^0.7.0
|
||||||
flutter_credit_card: ^4.0.1
|
flutter_credit_card: ^4.0.1
|
||||||
@@ -74,6 +73,10 @@ dependencies:
|
|||||||
flutter_graph_view: ^1.1.6
|
flutter_graph_view: ^1.1.6
|
||||||
flutter_animate: ^4.2.0
|
flutter_animate: ^4.2.0
|
||||||
flutter_flow_chart: 3.2.3
|
flutter_flow_chart: 3.2.3
|
||||||
|
video_player: ^2.6.0
|
||||||
|
chewie: ^1.0.0
|
||||||
|
shimmer: ^3.0.0
|
||||||
|
fl_chart: 0.69.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
@@ -83,6 +86,7 @@ dev_dependencies:
|
|||||||
|
|
||||||
dependency_overrides:
|
dependency_overrides:
|
||||||
intl: ^0.19.0 # Override para que pluto_grid funcione
|
intl: ^0.19.0 # Override para que pluto_grid funcione
|
||||||
|
video_player_web: ^2.3.0 # Override para compatibilidad con Flutter web
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
|||||||
Reference in New Issue
Block a user