import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; import 'package:manager_api_new/api.dart'; import 'package:mymuseum_visitapp/Helpers/translationHelper.dart'; import 'package:mymuseum_visitapp/Models/visitContext.dart'; import 'package:mymuseum_visitapp/Screens/Sections/Event/event_map_full_page.dart'; import 'package:mymuseum_visitapp/Screens/Sections/GuidedPath/guided_path_map_progression_page.dart'; import 'package:mymuseum_visitapp/constants.dart'; class EventPage extends StatefulWidget { const EventPage({ Key? key, required this.section, required this.visitAppContextIn, }) : super(key: key); final SectionEventDTO section; final VisitAppContext visitAppContextIn; @override State createState() => _EventPageState(); } class _EventPageState extends State { List _paths = []; @override void initState() { super.initState(); _loadPaths(); } Future _loadPaths() async { if (widget.section.id == null) return; if (widget.section.parcoursIds?.isEmpty ?? true) return; try { final paths = await widget.visitAppContextIn.clientAPI.sectionMapApi! .sectionMapGetAllGuidedPathFromSection(widget.section.id!); if (mounted) setState(() => _paths = paths ?? []); } catch (_) {} } ProgrammeBlock? get _activeBlock { final now = DateTime.now(); return widget.section.programme ?.where((b) => b.startTime != null && b.endTime != null && !now.isBefore(b.startTime!) && !now.isAfter(b.endTime!)) .firstOrNull; } String _formatTime(DateTime? dt) { if (dt == null) return ''; return '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}'; } String _formatDate(DateTime dt) => '${dt.day.toString().padLeft(2, '0')}/${dt.month.toString().padLeft(2, '0')}/${dt.year}'; String _formatDateRange() { final start = widget.section.startDate; final end = widget.section.endDate; if (start == null && end == null) return ''; if (start == null) return _formatDate(end!); if (end == null) return _formatDate(start); return '${_formatDate(start)} → ${_formatDate(end)}'; } MapDTO _minimalMapDTO() => MapDTO( latitude: widget.section.latitude, longitude: widget.section.longitude, zoom: 14, ); @override Widget build(BuildContext context) { final hasProgramme = widget.section.programme?.isNotEmpty ?? false; final annotations = widget.section.globalMapAnnotations ?? []; final hasMapSection = annotations.isNotEmpty || widget.section.baseSectionMapId != null || widget.section.latitude != null || _paths.isNotEmpty; final hasDescription = widget.section.description?.isNotEmpty ?? false; return Scaffold( backgroundColor: const Color(0xFF111111), body: CustomScrollView( slivers: [ _buildHero(context), SliverToBoxAdapter( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (hasProgramme) _buildProgrammeSection(), if (hasMapSection) _buildMapParcoursSection(context, annotations), if (hasDescription) _buildDescriptionSection(), const SizedBox(height: 48), ], ), ), ], ), ); } // ─── Hero ────────────────────────────────────────────────────────────────── Widget _buildHero(BuildContext context) { final title = _safeTranslate(widget.section.title); final dateRange = _formatDateRange(); final expandedHeight = MediaQuery.of(context).size.height * 0.52; return SliverAppBar( automaticallyImplyLeading: false, expandedHeight: expandedHeight, pinned: true, backgroundColor: kMainColor, title: Text( title, style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600), maxLines: 1, overflow: TextOverflow.ellipsis, ), leading: Padding( padding: const EdgeInsets.all(8), child: GestureDetector( onTap: () => Navigator.of(context).pop(), child: Container( decoration: const BoxDecoration(color: kMainColor, shape: BoxShape.circle), child: const Icon(Icons.arrow_back, color: Colors.white), ), ), ), flexibleSpace: FlexibleSpaceBar( collapseMode: CollapseMode.parallax, background: Stack( fit: StackFit.expand, children: [ if (widget.section.imageSource != null) Image.network( widget.section.imageSource!, fit: BoxFit.cover, errorBuilder: (_, __, ___) => _gradientBackground(), ) else _gradientBackground(), const DecoratedBox( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Colors.transparent, Colors.black87], stops: [0.35, 1.0], ), ), ), Positioned( bottom: 20, left: 16, right: 16, child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( title, style: const TextStyle( color: Colors.white, fontSize: 26, fontWeight: FontWeight.bold, height: 1.2, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), if (dateRange.isNotEmpty) ...[ const SizedBox(height: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: kMainColor.withOpacity(0.85), borderRadius: BorderRadius.circular(20), ), child: Text( dateRange, style: const TextStyle( color: Colors.white, fontSize: 13, fontWeight: FontWeight.w500, ), ), ), ], ], ), ), ], ), ), ); } Widget _gradientBackground() => Container( decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [kMainColor, kSecondColor], ), ), ); // ─── Programme ───────────────────────────────────────────────────────────── Widget _buildProgrammeSection() { final blocks = widget.section.programme!; final active = _activeBlock; return Padding( padding: const EdgeInsets.only(top: 28), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _sectionHeader(Icons.schedule, 'Programme'), const SizedBox(height: 8), ...blocks.asMap().entries.map( (e) => _buildProgrammeBlock(e.value, e.value == active, e.key == blocks.length - 1), ), ], ), ); } Widget _buildProgrammeBlock(ProgrammeBlock block, bool isActive, bool isLast) { final title = _safeTranslate(block.title); final startStr = _formatTime(block.startTime); final endStr = _formatTime(block.endTime); return IntrinsicHeight( child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 56, child: Padding( padding: const EdgeInsets.only(top: 14), child: Text( startStr, style: TextStyle( color: isActive ? kMainColor : Colors.grey[500], fontSize: 12, fontWeight: isActive ? FontWeight.bold : FontWeight.normal, ), textAlign: TextAlign.center, ), ), ), Column( children: [ const SizedBox(height: 16), Container( width: 12, height: 12, decoration: BoxDecoration( color: isActive ? kMainColor : Colors.grey[700], shape: BoxShape.circle, ), ), if (!isLast) Expanded( child: Container(width: 2, color: Colors.grey[800]), ), ], ), const SizedBox(width: 12), Expanded( child: GestureDetector( onTap: () => _showBlockDetail(block), child: Container( margin: EdgeInsets.only(bottom: isLast ? 0 : 12, right: 16, top: 4), padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: isActive ? kMainColor : const Color(0xFF1e1e1e), borderRadius: BorderRadius.circular(12), border: isActive ? null : Border.all(color: Colors.grey[800]!, width: 1), ), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: TextStyle( color: isActive ? Colors.white : Colors.white.withOpacity(0.9), fontWeight: FontWeight.w600, fontSize: 14, ), ), if (startStr.isNotEmpty && endStr.isNotEmpty) ...[ const SizedBox(height: 2), Text( '$startStr – $endStr', style: TextStyle( color: isActive ? Colors.white70 : Colors.grey[600], fontSize: 12, ), ), ], ], ), ), if (isActive) Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: Colors.white.withOpacity(0.25), borderRadius: BorderRadius.circular(12), ), child: const Text( 'En cours', style: TextStyle( color: Colors.white, fontSize: 11, fontWeight: FontWeight.w600, ), ), ) else Icon(Icons.chevron_right, color: Colors.grey[700], size: 18), ], ), ), ), ), ], ), ); } void _showBlockDetail(ProgrammeBlock block) { final title = _safeTranslate(block.title); final desc = _safeTranslate(block.description); final annotations = block.mapAnnotations ?? []; showModalBottomSheet( context: context, backgroundColor: const Color(0xFF1e1e1e), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), isScrollControlled: true, builder: (_) => Padding( padding: const EdgeInsets.fromLTRB(20, 16, 20, 40), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Center( child: Container( width: 40, height: 4, decoration: BoxDecoration( color: Colors.grey[700], borderRadius: BorderRadius.circular(2), ), ), ), const SizedBox(height: 16), Text( title, style: const TextStyle( color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold, ), ), if (desc.isNotEmpty) ...[ const SizedBox(height: 10), Text( desc, style: TextStyle(color: Colors.grey[400], fontSize: 14, height: 1.6), ), ], if (annotations.isNotEmpty) ...[ const SizedBox(height: 16), Divider(color: Colors.grey[800]), const SizedBox(height: 8), ...annotations.map((a) { final label = _safeTranslate(a.label); if (label.isEmpty) return const SizedBox.shrink(); return Padding( padding: const EdgeInsets.only(bottom: 8), child: Row( children: [ Icon(Icons.place, color: kMainColor, size: 16), const SizedBox(width: 8), Expanded( child: Text( label, style: const TextStyle(color: Colors.white, fontSize: 13), ), ), ], ), ); }), ], ], ), ), ); } // ─── Carte & Parcours ────────────────────────────────────────────────────── Widget _buildMapParcoursSection(BuildContext context, List annotations) { final lat = double.tryParse(widget.section.latitude ?? ''); final lng = double.tryParse(widget.section.longitude ?? ''); final center = (lat != null && lng != null) ? LatLng(lat, lng) : const LatLng(50.85, 4.35); return Padding( padding: const EdgeInsets.only(top: 28), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _sectionHeader(Icons.map_outlined, 'Carte & Parcours'), const SizedBox(height: 12), GestureDetector( onTap: () => Navigator.of(context).push(MaterialPageRoute( builder: (_) => EventMapFullPage( section: widget.section, visitAppContextIn: widget.visitAppContextIn, ), )), child: Container( height: 220, margin: const EdgeInsets.symmetric(horizontal: 16), clipBehavior: Clip.antiAlias, decoration: BoxDecoration(borderRadius: BorderRadius.circular(16)), child: Stack( children: [ FlutterMap( options: MapOptions( initialCenter: center, initialZoom: 14, interactionOptions: const InteractionOptions(flags: InteractiveFlag.none), ), children: [ TileLayer( urlTemplate: 'https://mt1.google.com/vt/lyrs=m&x={x}&y={y}&z={z}', userAgentPackageName: 'be.unov.myinfomate.visitapp', ), if (annotations.isNotEmpty) ...[ MarkerLayer(markers: _buildMarkers(annotations)), PolylineLayer(polylines: _buildPolylines(annotations)), ], ], ), Positioned( bottom: 8, right: 8, child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), decoration: BoxDecoration( color: Colors.black54, borderRadius: BorderRadius.circular(12), ), child: const Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.open_in_full, color: Colors.white, size: 12), SizedBox(width: 4), Text( 'Agrandir', style: TextStyle(color: Colors.white, fontSize: 11), ), ], ), ), ), ], ), ), ), if (_paths.isNotEmpty) ...[ const SizedBox(height: 16), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( '${_paths.length} parcours disponible${_paths.length > 1 ? 's' : ''}', style: TextStyle(color: Colors.grey[400], fontSize: 13), ), GestureDetector( onTap: () => _showParcoursList(context), child: Text( 'Voir tout', style: TextStyle( color: kMainColor, fontSize: 13, fontWeight: FontWeight.w600, ), ), ), ], ), ), const SizedBox(height: 10), SizedBox( height: 84, child: ListView.separated( padding: const EdgeInsets.symmetric(horizontal: 16), scrollDirection: Axis.horizontal, itemCount: _paths.length, separatorBuilder: (_, __) => const SizedBox(width: 10), itemBuilder: (_, i) => _buildParcoursChip(context, _paths[i]), ), ), ], ], ), ); } List _buildMarkers(List annotations) { final markers = []; for (final ann in annotations) { if (ann.geometryType?.value != 0) continue; final coords = ann.geometry?.coordinates; if (coords is! List || coords.length < 2) continue; final lat = (coords[1] as num).toDouble(); final lng = (coords[0] as num).toDouble(); markers.add(Marker( point: LatLng(lat, lng), width: 30, height: 30, child: Icon(Icons.place, color: kMainColor, size: 30), )); } return markers; } List _buildPolylines(List annotations) { final lines = []; for (final ann in annotations) { if (ann.geometryType?.value != 1) continue; final coords = ann.geometry?.coordinates; if (coords is! List) continue; final points = []; for (final c in coords) { if (c is List && c.length >= 2) { points.add(LatLng((c[1] as num).toDouble(), (c[0] as num).toDouble())); } } if (points.length < 2) continue; final color = ann.polyColor != null ? _hexColor(ann.polyColor!) : kMainColor; lines.add(Polyline(points: points, color: color, strokeWidth: 3.5)); } return lines; } Color _hexColor(String hex) { final v = int.tryParse('FF${hex.replaceAll('#', '')}', radix: 16); return v != null ? Color(v) : kMainColor; } Widget _buildParcoursChip(BuildContext context, GuidedPathDTO path) { final title = _safeTranslate(path.title); final stepCount = path.steps?.length ?? 0; return GestureDetector( onTap: () => Navigator.of(context).push(MaterialPageRoute( builder: (_) => GuidedPathMapProgressionPage( path: path, mapDTO: _minimalMapDTO(), visitAppContext: widget.visitAppContextIn, ), )), child: Container( width: 160, padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: const Color(0xFF1e1e1e), borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.grey[800]!, width: 1), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.route, color: kMainColor, size: 15), const SizedBox(width: 5), Text( '$stepCount étape${stepCount > 1 ? 's' : ''}', style: TextStyle(color: Colors.grey[500], fontSize: 11), ), ], ), const SizedBox(height: 6), Text( title, style: const TextStyle( color: Colors.white, fontSize: 13, fontWeight: FontWeight.w500, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), ], ), ), ); } void _showParcoursList(BuildContext context) { showModalBottomSheet( context: context, backgroundColor: const Color(0xFF1e1e1e), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), builder: (_) => Padding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 32), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Center( child: Container( width: 40, height: 4, decoration: BoxDecoration( color: Colors.grey[700], borderRadius: BorderRadius.circular(2), ), ), ), const SizedBox(height: 16), Row( children: [ Icon(Icons.route, size: 20, color: kMainColor), const SizedBox(width: 8), const Text( 'Parcours disponibles', style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600), ), ], ), const SizedBox(height: 8), ..._paths.map((path) { final title = _safeTranslate(path.title); final desc = _safeTranslate(path.description); final stepCount = path.steps?.length ?? 0; return ListTile( contentPadding: EdgeInsets.zero, leading: Container( width: 40, height: 40, decoration: BoxDecoration( color: kMainColor.withOpacity(0.15), shape: BoxShape.circle, ), child: Icon(Icons.map_outlined, color: kMainColor, size: 20), ), title: Text( title, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w500), ), subtitle: Text( desc.isNotEmpty ? desc : '$stepCount étape${stepCount > 1 ? 's' : ''}', maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(color: Colors.grey[500]), ), trailing: Icon(Icons.arrow_forward_ios, size: 14, color: Colors.grey[600]), onTap: () { Navigator.of(context).pop(); Navigator.of(context).push(MaterialPageRoute( builder: (_) => GuidedPathMapProgressionPage( path: path, mapDTO: _minimalMapDTO(), visitAppContext: widget.visitAppContextIn, ), )); }, ); }), ], ), ), ); } // ─── Description ─────────────────────────────────────────────────────────── Widget _buildDescriptionSection() { final desc = _safeTranslate(widget.section.description); if (desc.isEmpty) return const SizedBox.shrink(); return Padding( padding: const EdgeInsets.fromLTRB(16, 28, 16, 0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.info_outline, color: kMainColor, size: 20), const SizedBox(width: 8), const Text( 'À propos', style: TextStyle(color: Colors.white, fontSize: 17, fontWeight: FontWeight.bold), ), ], ), const SizedBox(height: 12), Text(desc, style: TextStyle(color: Colors.grey[300], fontSize: 14, height: 1.6)), ], ), ); } // ─── Helpers ─────────────────────────────────────────────────────────────── Widget _sectionHeader(IconData icon, String label) => Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( children: [ Icon(icon, color: kMainColor, size: 20), const SizedBox(width: 8), Text( label, style: const TextStyle(color: Colors.white, fontSize: 17, fontWeight: FontWeight.bold), ), ], ), ); String _safeTranslate(List? list) { if (list == null || list.isEmpty) return ''; try { return TranslationHelper.get(list, widget.visitAppContextIn); } catch (_) { return list.first.value ?? ''; } } }