Files
wortis_tpe/lib/pages/home.dart

1452 lines
55 KiB
Dart

// ===== lib/pages/home.dart AVEC SUPPORT ENTREPRISE =====
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../main.dart';
import '../widgets/balance_widget.dart';
import '../pages/form_service.dart';
import '../widgets/responsive_helper.dart';
class HomePage extends StatefulWidget {
final bool showAppBar;
const HomePage({super.key, this.showAppBar = true});
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
late ScrollController _scrollController;
final TextEditingController _searchController = TextEditingController();
static const Color primaryColor = Color(0xFF006699);
static const Color secondaryColor = Color(0xFF0088CC);
static const Color backgroundColor = Color(0xFFF8FAFC);
static const Color surfaceColor = Colors.white;
static const Color successColor = Color(0xFF10B981);
static const Color cardShadowColor = Color(0x08000000);
static const Color enterpriseColor = Color(
0xFF006699,
); // Violet pour entreprise
@override
void initState() {
super.initState();
_setupAnimations();
_setupScrollController();
_initializeOrientations();
_loadServices();
}
void _initializeOrientations() {
WidgetsBinding.instance.addPostFrameCallback((_) {
ResponsiveHelper.initializeOrientations(context);
});
}
void _setupAnimations() {
_animationController = AnimationController(
duration: Duration(milliseconds: 800),
vsync: this,
);
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeOut),
);
_slideAnimation = Tween<Offset>(
begin: Offset(0, 0.05),
end: Offset.zero,
).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeOut),
);
_animationController.forward();
}
void _setupScrollController() {
_scrollController = ScrollController();
}
void _loadServices() {
WidgetsBinding.instance.addPostFrameCallback((_) {
final servicesController = Provider.of<ServicesController>(
context,
listen: false,
);
servicesController.loadServices();
});
}
@override
void dispose() {
_animationController.dispose();
_searchController.dispose();
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final screenHeight = MediaQuery.of(context).size.height;
final screenWidth = MediaQuery.of(context).size.width;
final isTablet = screenWidth > 600;
return Scaffold(
backgroundColor: backgroundColor,
body: FadeTransition(
opacity: _fadeAnimation,
child: SlideTransition(
position: _slideAnimation,
child:
widget.showAppBar
? _buildWithAppBar(isTablet)
: _buildWithSliverAppBar(screenHeight, isTablet),
),
),
);
}
Widget _buildWithAppBar(bool isTablet) {
return Column(
children: [
_buildBeautifulAppBar(),
Expanded(
child: SingleChildScrollView(
padding: EdgeInsets.all(isTablet ? 24 : 20),
child: Column(
children: [
_buildModernSearchBar(),
SizedBox(height: 25),
_buildStylishCategoryTabs(),
SizedBox(height: 30),
_buildServicesContent(),
SizedBox(height: 20),
],
),
),
),
],
);
}
Widget _buildWithSliverAppBar(double screenHeight, bool isTablet) {
return CustomScrollView(
controller: _scrollController,
slivers: [
SliverAppBar(
expandedHeight: screenHeight * (isTablet ? 0.28 : 0.32),
floating: false,
pinned: true,
elevation: 0,
backgroundColor: primaryColor,
flexibleSpace: FlexibleSpaceBar(
background: _buildBeautifulHeader(),
collapseMode: CollapseMode.parallax,
),
bottom: PreferredSize(
preferredSize: Size.fromHeight(0),
child: Container(
height: 30,
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(30),
topRight: Radius.circular(30),
),
),
),
),
),
SliverToBoxAdapter(
child: Container(
color: backgroundColor,
padding: EdgeInsets.fromLTRB(
isTablet ? 24 : 20,
0,
isTablet ? 24 : 20,
isTablet ? 24 : 20,
),
child: Column(
children: [
_buildModernSearchBar(),
SizedBox(height: 25),
_buildStylishCategoryTabs(),
SizedBox(height: 30),
_buildServicesContent(),
SizedBox(height: 120),
],
),
),
),
],
);
}
// ===== HEADER AMÉLIORÉ AVEC SUPPORT ENTREPRISE =====
Widget _buildBeautifulHeader() {
return Consumer<AuthController>(
builder: (context, authController, child) {
final isEnterprise = authController.isEnterpriseMember;
return Container(
width: double.infinity,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors:
isEnterprise
? [
enterpriseColor,
enterpriseColor.withOpacity(0.8),
secondaryColor,
]
: [
primaryColor,
primaryColor.withOpacity(0.8),
secondaryColor,
],
stops: [0.0, 0.6, 1.0],
),
),
child: SafeArea(
child: Padding(
padding: EdgeInsets.fromLTRB(20, 10, 20, 30),
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
// Partie supérieure avec bienvenue et solde
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Badge type de compte
Container(
padding: EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: Colors.white.withOpacity(0.3),
width: 1,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
isEnterprise
? Icons.business
: Icons.person,
color: Colors.white,
size: 14,
),
SizedBox(width: 6),
Text(
isEnterprise
? 'Compte Entreprise'
: 'TPE Wortis',
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
],
),
),
SizedBox(height: 8),
// Nom de l'agent
Text(
authController.agentName ?? "Agent",
style: TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
letterSpacing: 0.5,
),
overflow: TextOverflow.ellipsis,
),
// Nom de l'entreprise si membre
if (isEnterprise &&
authController.enterprise?.nomEntreprise !=
null) ...[
SizedBox(height: 4),
Row(
children: [
Icon(
Icons.apartment,
color: Colors.white.withOpacity(0.8),
size: 14,
),
SizedBox(width: 6),
Expanded(
child: Text(
authController
.enterprise!
.nomEntreprise,
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 14,
fontWeight: FontWeight.w500,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
],
],
),
),
// Widget de solde
Flexible(
child: Container(
padding: EdgeInsets.all(3),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.15),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: Colors.white.withOpacity(0.2),
width: 1,
),
),
child: BalanceWidget(
showIcon: true,
compact: true,
textColor: Colors.white,
backgroundColor: Colors.transparent,
),
),
),
],
),
SizedBox(height: 20),
// Statistiques
_buildStatsRow(authController),
],
),
),
),
),
);
},
);
}
// ===== NOUVELLE MÉTHODE: LIGNE DE STATISTIQUES =====
Widget _buildStatsRow(AuthController authController) {
return Consumer<ServicesController>(
builder: (context, servicesController, child) {
final activeCount = servicesController.activeServices.length;
final totalCount = servicesController.allServices.length;
final isEnterprise = authController.isEnterpriseMember;
// Version compacte pour petits écrans
return LayoutBuilder(
builder: (context, constraints) {
final isVerySmall = constraints.maxWidth < 350;
return Container(
padding: EdgeInsets.symmetric(
horizontal: isVerySmall ? 8 : 12,
vertical: isVerySmall ? 8 : 10,
),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.15),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: Colors.white.withOpacity(0.2),
width: 1,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildCompactStatItem(
icon: Icons.verified_user_rounded,
value: '$activeCount',
isVerySmall: isVerySmall,
),
_buildStatDivider(),
_buildCompactStatItem(
icon: Icons.apps_rounded,
value: '$totalCount',
isVerySmall: isVerySmall,
),
_buildStatDivider(),
_buildCompactStatItem(
icon:
isEnterprise
? Icons.business_center
: Icons.category_rounded,
value:
isEnterprise
? 'PRO'
: '${servicesController.sectors.length - 1}',
isVerySmall: isVerySmall,
),
],
),
);
},
);
},
);
}
Widget _buildCompactStatItem({
required IconData icon,
required String value,
required bool isVerySmall,
}) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, color: Colors.white, size: isVerySmall ? 16 : 18),
SizedBox(height: isVerySmall ? 2 : 4),
Text(
value,
style: TextStyle(
color: Colors.white,
fontSize: isVerySmall ? 14 : 16,
fontWeight: FontWeight.bold,
),
),
],
);
}
Widget _buildStatDivider() {
return Container(
height: 35, // Réduit de 25 à 35
width: 1,
color: Colors.white.withOpacity(0.3),
margin: EdgeInsets.symmetric(horizontal: 4), // Réduit de 8 à 4
);
}
Widget _buildStatItem({
required IconData icon,
required String value,
required String label,
}) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 4), // Ajout de padding
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center, // AJOUTÉ
crossAxisAlignment: CrossAxisAlignment.center, // AJOUTÉ
children: [
Icon(icon, color: Colors.white, size: 18), // Réduit de 20 à 18
SizedBox(height: 4),
Text(
value,
style: TextStyle(
color: Colors.white,
fontSize: 16, // Réduit de 18 à 16
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 2),
Text(
label,
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 10, // Réduit de 11 à 10
height: 1.2, // Ajusté
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
);
}
// ===== APP BAR SIMPLIFIÉE AVEC SUPPORT ENTREPRISE =====
Widget _buildBeautifulAppBar() {
return Consumer<AuthController>(
builder: (context, authController, child) {
final isEnterprise = authController.isEnterpriseMember;
return Container(
height: 140,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors:
isEnterprise
? [enterpriseColor, secondaryColor]
: [primaryColor, secondaryColor],
),
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(25),
bottomRight: Radius.circular(25),
),
),
child: SafeArea(
child: Padding(
padding: EdgeInsets.fromLTRB(20, 20, 20, 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Badge type de compte
Container(
padding: EdgeInsets.symmetric(
horizontal: 10,
vertical: 4,
),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
isEnterprise ? Icons.business : Icons.person,
color: Colors.white,
size: 12,
),
SizedBox(width: 4),
Text(
isEnterprise ? 'Entreprise' : 'TPE Wortis',
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 10,
fontWeight: FontWeight.w600,
),
),
],
),
),
SizedBox(height: 5),
// Nom agent
Text(
authController.agentName ?? "Agent",
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,
),
// Nom entreprise si applicable
if (isEnterprise &&
authController.enterprise?.nomEntreprise !=
null) ...[
SizedBox(height: 2),
Text(
authController.enterprise!.nomEntreprise,
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 12,
),
overflow: TextOverflow.ellipsis,
),
],
],
),
),
BalanceWidget(
showIcon: true,
compact: true,
textColor: Colors.white,
backgroundColor: Colors.white.withOpacity(0.2),
),
],
),
),
),
);
},
);
}
Widget _buildModernSearchBar() {
return Container(
decoration: BoxDecoration(
color: surfaceColor,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: cardShadowColor,
blurRadius: 20,
offset: Offset(0, 8),
spreadRadius: 0,
),
],
),
child: Consumer<ServicesController>(
builder: (context, servicesController, child) {
return TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: '🔍 Rechercher un service...',
hintStyle: TextStyle(color: Colors.grey[400], fontSize: 15),
suffixIcon:
servicesController.searchQuery.isNotEmpty
? IconButton(
icon: Icon(Icons.clear_rounded, color: Colors.grey),
onPressed: () {
_searchController.clear();
servicesController.clearSearch();
},
)
: IconButton(
icon: Icon(Icons.refresh_rounded, color: primaryColor),
onPressed:
servicesController.isLoading
? null
: () => servicesController.refreshServices(),
),
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(
horizontal: 25,
vertical: 18,
),
),
onChanged: (value) {
servicesController.updateSearchQuery(value);
},
);
},
),
);
}
Widget _buildStylishCategoryTabs() {
return Consumer<ServicesController>(
builder: (context, servicesController, child) {
final sectors = servicesController.sectors;
final screenWidth = MediaQuery.of(context).size.width;
final isTablet = screenWidth > 600;
return SizedBox(
height: isTablet ? 70 : 55,
child: ListView.builder(
scrollDirection: Axis.horizontal,
padding: EdgeInsets.symmetric(horizontal: 5),
itemCount: sectors.length,
itemBuilder: (context, index) {
final sector = sectors[index];
final isSelected = servicesController.selectedSector == sector;
final count =
sector == 'Tous'
? servicesController.allServices.length
: servicesController.servicesCountBySector[sector] ?? 0;
return Padding(
padding: EdgeInsets.only(right: 15),
child: GestureDetector(
onTap: () {
servicesController.selectSector(sector);
},
child: AnimatedContainer(
duration: Duration(milliseconds: 200),
padding: EdgeInsets.symmetric(
horizontal: isTablet ? 30 : 20,
vertical: isTablet ? 20 : 15,
),
decoration: BoxDecoration(
gradient:
isSelected
? LinearGradient(
colors: [primaryColor, secondaryColor],
)
: null,
color: isSelected ? null : surfaceColor,
borderRadius: BorderRadius.circular(25),
boxShadow: [
BoxShadow(
color:
isSelected
? primaryColor.withOpacity(0.3)
: cardShadowColor,
blurRadius: isSelected ? 15 : 10,
offset: Offset(0, isSelected ? 8 : 4),
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Text(
sector,
style: TextStyle(
color: isSelected ? Colors.white : Colors.black87,
fontWeight: FontWeight.w600,
fontSize: isTablet ? 16 : 14,
),
overflow: TextOverflow.ellipsis,
),
),
if (count > 0) ...[
SizedBox(width: isTablet ? 10 : 8),
Container(
padding: EdgeInsets.symmetric(
horizontal: isTablet ? 8 : 6,
vertical: isTablet ? 3 : 2,
),
decoration: BoxDecoration(
color:
isSelected
? Colors.white.withOpacity(0.25)
: primaryColor.withOpacity(0.15),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'$count',
style: TextStyle(
color: isSelected ? Colors.white : primaryColor,
fontSize: isTablet ? 12 : 11,
fontWeight: FontWeight.bold,
),
),
),
],
],
),
),
),
);
},
),
);
},
);
}
Widget _buildServicesContent() {
return Consumer<ServicesController>(
builder: (context, servicesController, child) {
if (servicesController.isLoading) {
return _buildLoadingState();
}
if (servicesController.error != null) {
return _buildErrorState(servicesController);
}
final services = servicesController.filteredServices;
if (services.isEmpty) {
return _buildEmptyState();
}
return _buildBeautifulServicesGrid(services);
},
);
}
Widget _buildBeautifulServicesGrid(List<WortisService> services) {
final screenWidth = MediaQuery.of(context).size.width;
final screenHeight = MediaQuery.of(context).size.height;
final isLandscape = screenWidth > screenHeight;
int crossAxisCount = 2;
double childAspectRatio = 0.75;
double spacing = 16;
if (screenWidth > 1200) {
crossAxisCount = 5;
childAspectRatio = 0.85;
spacing = 24;
} else if (screenWidth > 900 && isLandscape) {
crossAxisCount = 4;
childAspectRatio = 0.82;
spacing = 20;
} else if (screenWidth > 800) {
crossAxisCount = 3;
childAspectRatio = 0.85;
spacing = 20;
} else if (screenWidth > 600) {
crossAxisCount = 3;
childAspectRatio = 0.8;
spacing = 18;
} else if (screenWidth > 400) {
crossAxisCount = 2;
childAspectRatio = 0.8;
spacing = 16;
}
final sortedServices = [...services];
final servicesController = Provider.of<ServicesController>(
context,
listen: false,
);
if (servicesController.selectedSector == 'Tous') {
sortedServices.sort((a, b) {
if (a.secteurActivite == 'Service Public' &&
b.secteurActivite != 'Service Public') {
return -1;
}
if (a.secteurActivite != 'Service Public' &&
b.secteurActivite == 'Service Public') {
return 1;
}
return a.name.compareTo(b.name);
});
}
return Padding(
padding: EdgeInsets.symmetric(horizontal: 4),
child: GridView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
crossAxisSpacing: spacing,
mainAxisSpacing: spacing,
childAspectRatio: childAspectRatio,
),
itemCount: sortedServices.length,
itemBuilder: (context, index) {
final service = sortedServices[index];
return _buildGorgeousServiceCard(service);
},
),
);
}
// Les autres méthodes restent identiques
Widget _buildGorgeousServiceCard(WortisService service) {
return LayoutBuilder(
builder: (context, constraints) {
final cardWidth = constraints.maxWidth;
final isTablet = cardWidth > 200;
final isVerySmall = cardWidth < 140;
final isTiny = cardWidth < 120; // NOUVEAU : pour très petits écrans
// Tailles adaptatives
final titleSize =
isTiny ? 11.0 : (isVerySmall ? 12.0 : (isTablet ? 18.0 : 14.0));
final sectorSize =
isTiny ? 8.0 : (isVerySmall ? 9.0 : (isTablet ? 12.0 : 9.0));
final buttonSize =
isTiny ? 10.0 : (isVerySmall ? 11.0 : (isTablet ? 14.0 : 11.0));
final statusSize =
isTiny ? 8.0 : (isVerySmall ? 9.0 : (isTablet ? 9.0 : 9.0));
final iconSize =
isTiny ? 8.0 : (isVerySmall ? 10.0 : (isTablet ? 14.0 : 10.0));
final buttonIconSize =
isTiny ? 12.0 : (isVerySmall ? 14.0 : (isTablet ? 15.0 : 16.0));
return Hero(
tag: service.name,
child: InkWell(
borderRadius: BorderRadius.circular(isTablet ? 28 : 25),
onTap: () {
if (service.status) {
Navigator.of(context).push(
MaterialPageRoute(
builder:
(context) => FormService(serviceName: service.name),
),
);
} else {
_showServiceUnavailableDialog(service);
}
},
child: Container(
decoration: BoxDecoration(
color: surfaceColor,
borderRadius: BorderRadius.circular(isTablet ? 28 : 25),
boxShadow: [
BoxShadow(
color:
service.status
? service.sectorColor.withOpacity(0.15)
: cardShadowColor,
blurRadius: 20,
offset: const Offset(0, 10),
),
],
border: Border.all(
color:
service.status
? service.sectorColor.withOpacity(0.2)
: Colors.grey.withOpacity(0.1),
width: 1.5,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// === IMAGE/BANNIÈRE ===
Flexible(
// CHANGÉ de Container à Flexible
flex: 3, // AJOUTÉ
child: Container(
width: double.infinity,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors:
service.status
? [
service.sectorColor.withOpacity(0.15),
service.sectorColor.withOpacity(0.05),
]
: [
Colors.grey.withOpacity(0.15),
Colors.grey.withOpacity(0.05),
],
),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(isTablet ? 26 : 23),
topRight: Radius.circular(isTablet ? 26 : 23),
),
),
child: Stack(
fit: StackFit.expand, // AJOUTÉ
children: [
ClipRRect(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(isTablet ? 26 : 23),
topRight: Radius.circular(isTablet ? 26 : 23),
),
child:
service.banner != null &&
service.banner!.isNotEmpty
? Image.network(
service.banner!,
fit: BoxFit.cover,
errorBuilder: (
context,
error,
stackTrace,
) {
return _buildIconFallback(
service,
isTablet,
isVerySmall,
);
},
loadingBuilder: (
context,
child,
loadingProgress,
) {
if (loadingProgress == null)
return child;
return Center(
child: CircularProgressIndicator(
strokeWidth: 2,
color: service.sectorColor,
value:
loadingProgress
.expectedTotalBytes !=
null
? loadingProgress
.cumulativeBytesLoaded /
loadingProgress
.expectedTotalBytes!
: null,
),
);
},
)
: _buildIconFallback(
service,
isTablet,
isVerySmall,
),
),
// Badge de statut
Positioned(
top: isTablet ? 12 : (isTiny ? 6 : 8),
right: isTablet ? 12 : (isTiny ? 6 : 8),
child: Container(
padding: EdgeInsets.symmetric(
horizontal:
isTablet
? 12
: (isTiny ? 4 : (isVerySmall ? 6 : 8)),
vertical:
isTablet
? 6
: (isTiny ? 2 : (isVerySmall ? 3 : 4)),
),
decoration: BoxDecoration(
color: (service.status
? successColor
: Colors.orange)
.withOpacity(0.9),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Colors.white.withOpacity(0.3),
width: 1,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 4,
offset: Offset(0, 2),
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: isTablet ? 8 : (isTiny ? 4 : 6),
height: isTablet ? 8 : (isTiny ? 4 : 6),
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
),
if (!isTiny && !isVerySmall) ...[
SizedBox(width: isTablet ? 6 : 4),
Text(
service.status ? 'Actif' : 'Inactif',
style: TextStyle(
color: Colors.white,
fontSize: statusSize,
fontWeight: FontWeight.w600,
),
),
],
],
),
),
),
],
),
),
),
// === CONTENU TEXTUEL ===
Flexible(
// CHANGÉ de Expanded à Flexible
flex: 4, // AJOUTÉ
child: Padding(
padding: EdgeInsets.all(
isTablet ? 16 : (isTiny ? 8 : (isVerySmall ? 10 : 12)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, // AJOUTÉ
children: [
// Nom du service
Flexible(
// CHANGÉ
child: Text(
service.name,
style: TextStyle(
fontSize: titleSize,
fontWeight: FontWeight.bold,
color: Colors.black87,
height: 1.1, // AJOUTÉ
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
SizedBox(height: isTablet ? 8 : (isTiny ? 4 : 6)),
// Secteur d'activité (masqué sur très petits écrans)
if (!isTiny)
Container(
padding: EdgeInsets.symmetric(
horizontal:
isTablet ? 12 : (isVerySmall ? 6 : 8),
vertical: isTablet ? 6 : (isVerySmall ? 3 : 4),
),
decoration: BoxDecoration(
color: service.sectorColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: service.sectorColor.withOpacity(0.3),
width: 0.5,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.business_center_rounded,
color: service.sectorColor,
size: iconSize,
),
SizedBox(width: isTablet ? 6 : 4),
Flexible(
child: Text(
service.secteurActivite,
style: TextStyle(
fontSize: sectorSize,
color: service.sectorColor,
fontWeight: FontWeight.w600,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
Spacer(), // AJOUTÉ pour pousser le bouton en bas
// Bouton d'action
Container(
width: double.infinity,
padding: EdgeInsets.symmetric(
vertical:
isTablet
? 12
: (isTiny ? 6 : (isVerySmall ? 8 : 10)),
),
decoration: BoxDecoration(
gradient:
service.status
? LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [
service.sectorColor.withOpacity(0.8),
service.sectorColor,
],
)
: null,
color:
service.status
? null
: Colors.grey.withOpacity(0.15),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color:
service.status
? Colors.transparent
: Colors.grey.withOpacity(0.2),
width: 1,
),
boxShadow:
service.status
? [
BoxShadow(
color: service.sectorColor
.withOpacity(0.25),
blurRadius: 6,
offset: const Offset(0, 3),
),
]
: [],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Icon(
service.status
? Icons.arrow_forward_rounded
: Icons.schedule_rounded,
color:
service.status
? Colors.white
: Colors.grey[600],
size: buttonIconSize,
),
if (!isTiny) ...[
SizedBox(width: isTablet ? 6 : 4),
Flexible(
// AJOUTÉ
child: Text(
service.status
? 'Accéder'
: 'Indisponible',
style: TextStyle(
color:
service.status
? Colors.white
: Colors.red[800],
fontSize: buttonSize,
fontWeight: FontWeight.w600,
letterSpacing: 0.3,
),
overflow: TextOverflow.ellipsis, // AJOUTÉ
),
),
],
],
),
),
],
),
),
),
],
),
),
),
);
},
);
}
Widget _buildIconFallback(
WortisService service,
bool isTablet,
bool isVerySmall,
) {
return Container(
width: double.infinity,
height: double.infinity,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors:
service.status
? [
service.sectorColor.withOpacity(0.2),
service.sectorColor.withOpacity(0.05),
]
: [
Colors.grey.withOpacity(0.2),
Colors.grey.withOpacity(0.05),
],
),
),
child: Center(
child: Icon(
service.flutterIcon,
color: service.status ? service.sectorColor : Colors.grey,
size: isVerySmall ? 32 : (isTablet ? 48 : 40),
),
),
);
}
Widget _buildLoadingState() {
return SizedBox(
height: 300,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 60,
height: 60,
child: CircularProgressIndicator(
color: primaryColor,
strokeWidth: 4,
),
),
SizedBox(height: 20),
Text(
'Chargement des services...',
style: TextStyle(
fontSize: 16,
color: Colors.grey[600],
fontWeight: FontWeight.w500,
),
),
],
),
),
);
}
Widget _buildErrorState(ServicesController servicesController) {
return Container(
height: 300,
padding: EdgeInsets.all(24),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 64, color: Colors.red[400]),
SizedBox(height: 16),
Text(
'Erreur de chargement',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
SizedBox(height: 8),
Text(
'Impossible de charger les services',
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
textAlign: TextAlign.center,
),
SizedBox(height: 24),
ElevatedButton.icon(
onPressed: () => servicesController.refreshServices(),
icon: Icon(Icons.refresh, size: 20),
label: Text('Réessayer'),
style: ElevatedButton.styleFrom(
backgroundColor: primaryColor,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
),
),
],
),
),
);
}
Widget _buildEmptyState() {
return SizedBox(
height: 250,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.search_off, size: 64, color: Colors.grey[400]),
SizedBox(height: 16),
Text(
'Aucun service trouvé',
style: TextStyle(
fontSize: 18,
color: Colors.grey[600],
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 8),
Text(
'Essayez avec d\'autres mots-clés',
style: TextStyle(fontSize: 14, color: Colors.grey[500]),
),
],
),
),
);
}
void _showServiceUnavailableDialog(WortisService service) {
showDialog(
context: context,
builder:
(context) => AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
title: Row(
children: [
Icon(Icons.warning, color: Colors.orange),
SizedBox(width: 8),
Expanded(
child: Text(
'Service indisponible',
style: TextStyle(fontSize: 16),
),
),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Le service "${service.name}" est actuellement indisponible.',
),
SizedBox(height: 12),
Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.orange.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Secteur: ${service.secteurActivite}',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
Text(
'Type: ${service.typeService}',
style: TextStyle(fontSize: 12),
),
],
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('Compris'),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
Icon(Icons.notifications_active, color: Colors.white),
SizedBox(width: 8),
Expanded(
child: Text(
'Vous serez notifié quand ${service.name} sera disponible',
),
),
],
),
backgroundColor: Colors.green,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: Text(
'Me notifier',
style: TextStyle(color: Colors.white),
),
),
],
),
);
}
}