mapa con marcador añadido beta
This commit is contained in:
293
assets/referencia/nethive_tablas_actualizado.md
Normal file
293
assets/referencia/nethive_tablas_actualizado.md
Normal file
@@ -0,0 +1,293 @@
|
||||
|
||||
# 📘 Estructura de Base de Datos — NETHIVE (Esquema: `nethive`)
|
||||
|
||||
---
|
||||
|
||||
## 🏢 `empresa`
|
||||
|
||||
| Columna | Tipo de dato | Descripción |
|
||||
|-----------------|----------------|-----------------------------------|
|
||||
| id | UUID | PRIMARY KEY |
|
||||
| nombre | TEXT | Nombre de la empresa |
|
||||
| rfc | TEXT | Registro fiscal |
|
||||
| direccion | TEXT | Dirección |
|
||||
| telefono | TEXT | Teléfono de contacto |
|
||||
| email | TEXT | Correo electrónico |
|
||||
| fecha_creacion | TIMESTAMP | Fecha de creación (default: now) |
|
||||
| logo_url | TEXT | URL del logo |
|
||||
| imagen_url | TEXT | URL de imagen principal |
|
||||
|
||||
---
|
||||
|
||||
## 🏪 `negocio`
|
||||
|
||||
| Columna | Tipo de dato | Descripción |
|
||||
|-----------------|----------------|-----------------------------------|
|
||||
| id | UUID | PRIMARY KEY |
|
||||
| empresa_id | UUID | FK → empresa.id |
|
||||
| nombre | TEXT | Nombre del negocio |
|
||||
| direccion | TEXT | Dirección |
|
||||
| latitud | DECIMAL(9,6) | Latitud geográfica |
|
||||
| longitud | DECIMAL(9,6) | Longitud geográfica |
|
||||
| tipo_local | TEXT | Tipo de local (Sucursal, etc.) |
|
||||
| fecha_creacion | TIMESTAMP | Default: now() |
|
||||
| logo_url | TEXT | Logo del negocio |
|
||||
| imagen_url | TEXT | Imagen del negocio |
|
||||
|
||||
---
|
||||
|
||||
## 🧾 `categoria_componente`
|
||||
|
||||
| Columna | Tipo de dato | Descripción |
|
||||
|---------|--------------|--------------------------|
|
||||
| id | SERIAL | PRIMARY KEY |
|
||||
| nombre | TEXT | Nombre único de categoría|
|
||||
|
||||
---
|
||||
|
||||
## 📦 `componente`
|
||||
|
||||
| Columna | Tipo de dato | Descripción |
|
||||
|-----------------|----------------|------------------------------------|
|
||||
| id | UUID | PRIMARY KEY |
|
||||
| negocio_id | UUID | FK → negocio.id |
|
||||
| categoria_id | INT | FK → categoria_componente.id |
|
||||
| nombre | TEXT | Nombre del componente |
|
||||
| descripcion | TEXT | Descripción general |
|
||||
| en_uso | BOOLEAN | Si está en uso |
|
||||
| activo | BOOLEAN | Si está activo |
|
||||
| ubicacion | TEXT | Ubicación física (rack, bandeja) |
|
||||
| imagen_url | TEXT | URL de imagen |
|
||||
| fecha_registro | TIMESTAMP | Default: now() |
|
||||
|
||||
---
|
||||
|
||||
## 🔌 `detalle_cable`
|
||||
|
||||
| Columna | Tipo de dato |
|
||||
|----------------|----------------|
|
||||
| componente_id | UUID (PK, FK) |
|
||||
| tipo_cable | TEXT |
|
||||
| color | TEXT |
|
||||
| tamaño | DECIMAL(5,2) |
|
||||
| tipo_conector | TEXT |
|
||||
|
||||
---
|
||||
|
||||
## 📶 `detalle_switch`
|
||||
|
||||
| Columna | Tipo de dato |
|
||||
|----------------------|----------------|
|
||||
| componente_id | UUID (PK, FK) |
|
||||
| marca | TEXT |
|
||||
| modelo | TEXT |
|
||||
| numero_serie | TEXT |
|
||||
| administrable | BOOLEAN |
|
||||
| poe | BOOLEAN |
|
||||
| cantidad_puertos | INT |
|
||||
| velocidad_puertos | TEXT |
|
||||
| tipo_puertos | TEXT |
|
||||
| ubicacion_en_rack | TEXT |
|
||||
| direccion_ip | TEXT |
|
||||
| firmware | TEXT |
|
||||
|
||||
---
|
||||
|
||||
## 🧱 `detalle_patch_panel`
|
||||
|
||||
| Columna | Tipo de dato |
|
||||
|---------------------|----------------|
|
||||
| componente_id | UUID (PK, FK) |
|
||||
| tipo_conector | TEXT |
|
||||
| numero_puertos | INT |
|
||||
| categoria | TEXT |
|
||||
| tipo_montaje | TEXT |
|
||||
| numeracion_frontal | BOOLEAN |
|
||||
| panel_ciego | BOOLEAN |
|
||||
|
||||
---
|
||||
|
||||
## 🗄 `detalle_rack`
|
||||
|
||||
| Columna | Tipo de dato |
|
||||
|------------------------|----------------|
|
||||
| componente_id | UUID (PK, FK) |
|
||||
| tipo | TEXT |
|
||||
| altura_u | INT |
|
||||
| profundidad_cm | INT |
|
||||
| ancho_cm | INT |
|
||||
| ventilacion_integrada | BOOLEAN |
|
||||
| puertas_con_llave | BOOLEAN |
|
||||
| ruedas | BOOLEAN |
|
||||
| color | TEXT |
|
||||
|
||||
---
|
||||
|
||||
## 🧰 `detalle_organizador`
|
||||
|
||||
| Columna | Tipo de dato |
|
||||
|--------------|----------------|
|
||||
| componente_id| UUID (PK, FK) |
|
||||
| tipo | TEXT |
|
||||
| material | TEXT |
|
||||
| tamaño | TEXT |
|
||||
| color | TEXT |
|
||||
|
||||
---
|
||||
|
||||
## ⚡ `detalle_ups`
|
||||
|
||||
| Columna | Tipo de dato |
|
||||
|--------------------|----------------|
|
||||
| componente_id | UUID (PK, FK) |
|
||||
| tipo | TEXT |
|
||||
| marca | TEXT |
|
||||
| modelo | TEXT |
|
||||
| voltaje_entrada | TEXT |
|
||||
| voltaje_salida | TEXT |
|
||||
| capacidad_va | INT |
|
||||
| autonomia_minutos | INT |
|
||||
| cantidad_tomas | INT |
|
||||
| rackeable | BOOLEAN |
|
||||
|
||||
---
|
||||
|
||||
## 🔐 `detalle_router_firewall`
|
||||
|
||||
| Columna | Tipo de dato |
|
||||
|--------------------------|----------------|
|
||||
| componente_id | UUID (PK, FK) |
|
||||
| tipo | TEXT |
|
||||
| marca | TEXT |
|
||||
| modelo | TEXT |
|
||||
| numero_serie | TEXT |
|
||||
| interfaces | TEXT |
|
||||
| capacidad_routing_gbps | DECIMAL(5,2) |
|
||||
| direccion_ip | TEXT |
|
||||
| firmware | TEXT |
|
||||
| licencias | TEXT |
|
||||
|
||||
---
|
||||
|
||||
## 🧿 `detalle_equipo_activo`
|
||||
|
||||
| Columna | Tipo de dato |
|
||||
|-------------------|----------------|
|
||||
| componente_id | UUID (PK, FK) |
|
||||
| tipo | TEXT |
|
||||
| marca | TEXT |
|
||||
| modelo | TEXT |
|
||||
| numero_serie | TEXT |
|
||||
| especificaciones | TEXT |
|
||||
| direccion_ip | TEXT |
|
||||
| firmware | TEXT |
|
||||
|
||||
---
|
||||
|
||||
## 🧭 `distribucion`
|
||||
|
||||
| Columna | Tipo de dato | Descripción |
|
||||
|--------------|----------------|--------------------------------------|
|
||||
| id | UUID | PRIMARY KEY |
|
||||
| negocio_id | UUID | FK → negocio.id |
|
||||
| tipo | TEXT | 'MDF' o 'IDF' |
|
||||
| nombre | TEXT | Nombre de la ubicación lógica |
|
||||
| descripcion | TEXT | Detalles adicionales (opcional) |
|
||||
|
||||
---
|
||||
|
||||
## 🔗 `conexion_componente`
|
||||
|
||||
| Columna | Tipo de dato | Descripción |
|
||||
|-----------------------|----------------|------------------------------------------|
|
||||
| id | UUID | PRIMARY KEY |
|
||||
| componente_origen_id | UUID | FK → componente.id |
|
||||
| componente_destino_id | UUID | FK → componente.id |
|
||||
| descripcion | TEXT | Descripción de la conexión (opcional) |
|
||||
| activo | BOOLEAN | Si la conexión está activa |
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
## 👁️ `vista_negocios_con_coordenadas`
|
||||
|
||||
| Columna | Tipo de dato | Descripción |
|
||||
|--------------------|--------------|--------------------------------------------|
|
||||
| negocio_id | UUID | ID del negocio |
|
||||
| nombre_negocio | TEXT | Nombre del negocio |
|
||||
| latitud | DECIMAL | Latitud del negocio |
|
||||
| longitud | DECIMAL | Longitud del negocio |
|
||||
| logo_negocio | TEXT | URL del logo del negocio |
|
||||
| imagen_negocio | TEXT | URL de la imagen del negocio |
|
||||
| empresa_id | UUID | ID de la empresa |
|
||||
| nombre_empresa | TEXT | Nombre de la empresa |
|
||||
| logo_empresa | TEXT | URL del logo de la empresa |
|
||||
| imagen_empresa | TEXT | URL de la imagen de la empresa |
|
||||
|
||||
---
|
||||
|
||||
## 📋 `vista_inventario_por_negocio`
|
||||
|
||||
| Columna | Tipo de dato | Descripción |
|
||||
|--------------------|--------------|---------------------------------------------|
|
||||
| componente_id | UUID | ID del componente |
|
||||
| nombre_componente | TEXT | Nombre del componente |
|
||||
| categoria | TEXT | Categoría del componente |
|
||||
| en_uso | BOOLEAN | Si está en uso |
|
||||
| activo | BOOLEAN | Si está activo |
|
||||
| ubicacion | TEXT | Ubicación física del componente |
|
||||
| imagen_componente | TEXT | Imagen asociada al componente |
|
||||
| negocio_id | UUID | ID del negocio |
|
||||
| nombre_negocio | TEXT | Nombre del negocio |
|
||||
| logo_negocio | TEXT | Logo del negocio |
|
||||
| imagen_negocio | TEXT | Imagen del negocio |
|
||||
| empresa_id | UUID | ID de la empresa |
|
||||
| nombre_empresa | TEXT | Nombre de la empresa |
|
||||
| logo_empresa | TEXT | Logo de la empresa |
|
||||
| imagen_empresa | TEXT | Imagen de la empresa |
|
||||
|
||||
---
|
||||
|
||||
## 🧵 `vista_detalle_cables`
|
||||
|
||||
| Columna | Tipo de dato | Descripción |
|
||||
|--------------------|--------------|--------------------------------------------|
|
||||
| componente_id | UUID | ID del componente |
|
||||
| nombre | TEXT | Nombre del cable |
|
||||
| tipo_cable | TEXT | Tipo de cable (UTP, fibra, etc.) |
|
||||
| color | TEXT | Color del cable |
|
||||
| tamaño | DECIMAL | Longitud del cable |
|
||||
| tipo_conector | TEXT | Tipo de conector (RJ45, LC, etc.) |
|
||||
| en_uso | BOOLEAN | Si está en uso |
|
||||
| activo | BOOLEAN | Si está activo |
|
||||
| ubicacion | TEXT | Ubicación física |
|
||||
| imagen_componente | TEXT | Imagen del cable |
|
||||
| nombre_negocio | TEXT | Nombre del negocio |
|
||||
| logo_negocio | TEXT | Logo del negocio |
|
||||
| nombre_empresa | TEXT | Nombre de la empresa |
|
||||
| logo_empresa | TEXT | Logo de la empresa |
|
||||
|
||||
---
|
||||
|
||||
## 📊 `vista_resumen_componentes_activos`
|
||||
|
||||
| Columna | Tipo de dato | Descripción |
|
||||
|------------------|--------------|----------------------------------------------|
|
||||
| nombre_empresa | TEXT | Nombre de la empresa |
|
||||
| nombre_negocio | TEXT | Nombre del negocio |
|
||||
| categoria | TEXT | Categoría del componente |
|
||||
| cantidad_activos | INTEGER | Cantidad total de componentes activos |
|
||||
|
||||
---
|
||||
|
||||
## 🔌 `vista_conexiones_por_negocio`
|
||||
|
||||
| Columna | Tipo de dato | Descripción |
|
||||
|-----------------------|--------------|------------------------------------------|
|
||||
| id | UUID | ID de la conexión |
|
||||
| componente_origen_id | UUID | Componente origen |
|
||||
| componente_destino_id | UUID | Componente destino |
|
||||
| descripcion | TEXT | Descripción de la conexión |
|
||||
| activo | BOOLEAN | Si la conexión está activa |
|
||||
"""
|
||||
@@ -5,8 +5,8 @@ import 'package:nethive_neo/pages/empresa_negocios/widgets/empresa_selector_side
|
||||
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';
|
||||
import 'package:nethive_neo/helpers/globals.dart';
|
||||
|
||||
class EmpresaNegociosPage extends StatefulWidget {
|
||||
const EmpresaNegociosPage({Key? key}) : super(key: key);
|
||||
@@ -55,7 +55,6 @@ class _EmpresaNegociosPageState extends State<EmpresaNegociosPage>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isLargeScreen = MediaQuery.of(context).size.width > 1200;
|
||||
final isMediumScreen = MediaQuery.of(context).size.width > 800;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppTheme.of(context).primaryBackground,
|
||||
@@ -589,85 +588,10 @@ class _EmpresaNegociosPageState extends State<EmpresaNegociosPage>
|
||||
}
|
||||
|
||||
Widget _buildMapView() {
|
||||
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,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppTheme.of(context).primaryColor.withOpacity(0.2),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Center(
|
||||
child: TweenAnimationBuilder<double>(
|
||||
duration: const Duration(milliseconds: 1500),
|
||||
tween: Tween(begin: 0.0, end: 1.0),
|
||||
builder: (context, value, child) {
|
||||
return Transform.scale(
|
||||
scale: 0.8 + (0.2 * value),
|
||||
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.map,
|
||||
size: 80,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'Vista de Mapa',
|
||||
style: TextStyle(
|
||||
color: AppTheme.of(context).primaryColor,
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
),
|
||||
child: Text(
|
||||
'Próximamente se implementará el mapa interactivo\ncon las ubicaciones de todas las sucursales',
|
||||
style: TextStyle(
|
||||
color: AppTheme.of(context).primaryColor,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
return Consumer<EmpresasNegociosProvider>(
|
||||
builder: (context, provider, child) {
|
||||
return NegociosMapView(provider: provider);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
685
lib/pages/empresa_negocios/widgets/negocios_map_view.dart
Normal file
685
lib/pages/empresa_negocios/widgets/negocios_map_view.dart
Normal file
@@ -0,0 +1,685 @@
|
||||
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/theme/theme.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 + 20, // Offset del cursor para evitar interferencia
|
||||
position.dy - 80, // Arriba 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(
|
||||
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 + 20,
|
||||
event.position.dy - 80,
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
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: Icon(
|
||||
Icons.store,
|
||||
color: Colors.white,
|
||||
size: isHovered ? 22 : 18,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
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: [
|
||||
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: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
negocio.nombre,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
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),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.business,
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
negocio.tipoLocal,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
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.touch_app,
|
||||
color: Colors.white,
|
||||
size: 14,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'Clic para ver infraestructura',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
80
pubspec.lock
80
pubspec.lock
@@ -193,6 +193,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
dart_earcut:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dart_earcut
|
||||
sha256: e485001bfc05dcbc437d7bfb666316182e3522d4c3f9668048e004d0eb2ce43b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
dart_jsonwebtoken:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -371,6 +379,14 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_map:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_map
|
||||
sha256: "2ecb34619a4be19df6f40c2f8dce1591675b4eff7a6857bd8f533706977385da"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.2"
|
||||
flutter_plugin_android_lifecycle:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -709,6 +725,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.1"
|
||||
latlong2:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: latlong2
|
||||
sha256: "98227922caf49e6056f91b6c56945ea1c7b166f28ffcd5fb8e72fc0b453cc8fe"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.1"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -741,6 +765,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
lists:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: lists
|
||||
sha256: "4ca5c19ae4350de036a7e996cdd1ee39c93ac0a2b840f4915459b7d0a7d4ab27"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
logger:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: logger
|
||||
sha256: "2621da01aabaf223f8f961e751f2c943dbb374dc3559b982f200ccedadaa6999"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.6.0"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -781,6 +821,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.12.0"
|
||||
mgrs_dart:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: mgrs_dart
|
||||
sha256: fb89ae62f05fa0bb90f70c31fc870bcbcfd516c843fb554452ab3396f78586f7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -893,6 +941,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.8.0"
|
||||
polylabel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: polylabel
|
||||
sha256: "41b9099afb2aa6c1730bdd8a0fab1400d287694ec7615dd8516935fa3144214b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
postgrest:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -909,6 +965,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.4.0"
|
||||
proj4dart:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: proj4dart
|
||||
sha256: c8a659ac9b6864aa47c171e78d41bbe6f5e1d7bd790a5814249e6b68bc44324e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1202,6 +1266,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.2"
|
||||
unicode:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: unicode
|
||||
sha256: "0f69e46593d65245774d4f17125c6084d2c20b4e473a983f6e21b7d7762218f1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.1"
|
||||
url_launcher:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1394,6 +1466,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.5.4"
|
||||
wkt_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: wkt_parser
|
||||
sha256: "8a555fc60de3116c00aad67891bcab20f81a958e4219cc106e3c037aa3937f13"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -40,6 +40,10 @@ dependencies:
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
|
||||
# Mapa interactivo
|
||||
flutter_map: ^7.0.2
|
||||
latlong2: ^0.9.1
|
||||
|
||||
# Paquetes adicionales del pubspec.yaml que te proporcionaron
|
||||
animated_toggle_switch: ^0.8.2
|
||||
another_stepper: ^1.2.2
|
||||
|
||||
Reference in New Issue
Block a user