Sitemap
4 min readJun 11, 2026

--

dart

import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
void main() => runApp(EcoGlowAppUltime());class EcoGlowAppUltime extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.green,
scaffoldBackgroundColor: Colors.grey[100],
),
home: EcranSelectionRole(),
);
}
}
// --- ÉCRAN DE SÉLECTION DU RÔLE (Pour l'utilité extrême sur le terrain) ---
class EcranSelectionRole extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text("EcoGlow", style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold, color: Colors.green)),
SizedBox(height: 10),
Text("Lavage Auto Mobile Éco-responsable", style: TextStyle(color: Colors.grey)),
SizedBox(height: 50),
ElevatedButton.icon(
icon: Icon(Icons.directions_car),
label: Text("ESPACE CLIENT"),
style: ElevatedButton.styleFrom(minimumSize: Size(250, 60), textStyle: TextStyle(fontSize: 18)),
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => EcranClient())),
),
SizedBox(height: 20),
OutlinedButton.icon(
icon: Icon(Icons.engineering),
label: Text("ESPACE EMPLOYÉ (LAVEUR)"),
style: OutlinedButton.styleFrom(minimumSize: Size(250, 60), textStyle: TextStyle(fontSize: 18)),
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => EcranLaveur())),
),
],
),
),
);
}
}
// --- 🚗 1. INTERFACE CLIENT (Commande, GPS & Paiement) ---
class EcranClient extends StatefulWidget {
@override
_EcranClientState createState() => _EcranClientState();
}
class _EcranClientState extends State<EcranClient> {
String formuleSelectionnee = "Standard";
int prix = 1000;
String statutCommande = "Aucune commande en cours";
Position? positionActuelle;
final _formKey = GlobalKey<FormState>();
final _plaqueController = TextEditingController();
final _telephoneController = TextEditingController();
@override
void initState() {
super.initState();
_recupererPositionGPS();
}
// Récupération de la position GPS réelle du client
Future<void> _recupererPositionGPS() async {
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) return;
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) return;
}
Position position = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high);
setState(() { positionActuelle = position; });
}
// Simulation de l'appel API CinetPay (Orange, MTN, Wave, Moov)
Future<void> _initialiserPaiementMobileMoney() async {
if (positionActuelle == null) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Erreur : GPS introuvable.")));
return;
}
setState(() { statutCommande = "Traitement du paiement Mobile Money..."; }); // Payload envoyé à la passerelle de paiement
final response = await http.post(
Uri.parse('https://cinetpay.com'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
"apikey": "VOTRE_CLE_API_CINETPAY",
"site_id": "VOTRE_SITE_ID",
"transaction_id": DateTime.now().millisecondsSinceEpoch.toString(),
"amount": prix,
"currency": "XOF",
"alternative_currency": "",
"description": "Lavage EcoGlow $formuleSelectionnee",
"customer_phone_number": _telephoneController.text,
"customer_name": "Client EcoGlow",
"notify_url": "https://votre-serveur.com"
}),
);
if (response.statusCode == 200 || response.statusCode == 201) {
// Si le paiement est validé, on envoie la commande au serveur pour le dispatch
_envoyerCommandeAuServeur();
} else {
setState(() { statutCommande = "Échec du paiement. Réessayez."; });
}
}
Future<void> _envoyerCommandeAuServeur() async {
setState(() { statutCommande = "Recherche du laveur le plus proche..."; });

// Appel à votre serveur Node.js codé précédemment
try {
final res = await http.post(
Uri.parse('http://votre-serveur-nodejs.com'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
"clientLat": positionActuelle!.latitude,
"clientLng": positionActuelle!.longitude,
"formule": formuleSelectionnee,
"plaque": _plaqueController.text
}),
);

final data = jsonDecode(res.body);
setState(() {
statutCommande = data['succes'] ? data['message'] : "Aucun laveur à moins de 5km. En attente.";
});
} catch (e) {
setState(() { statutCommande = "Commande validée ! (Simulation hors-ligne)"; });
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Commander un Lavage")),
body: SingleChildScrollView(
padding: EdgeInsets.all(20),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Card(
color: Colors.green[50],
child: Padding(
padding: EdgeInsets.all(15),
child: Row(
children: [
Icon(Icons.location_on, color: Colors.green),
SizedBox(width: 10),
Text(positionActuelle != null
? "Position GPS enregistrée ✓"
: "Recherche de votre position GPS...",
style: TextStyle(fontWeight: FontWeight.bold),
)
],
),
),
),
SizedBox(height: 20),
Text("1. Formule de lavage", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
Row(
children: [
ChoiceChip(
label: Text("Standard (1 000 F)"),
selected: formuleSelectionnee == "Standard",
onSelected: (val) => setState(() { formuleSelectionnee = "Standard"; prix = 1000; }),
),
SizedBox(width: 10),
ChoiceChip(
label: Text("Premium (2 000 F)"),
selected: formuleSelectionnee == "Premium",
onSelected: (val) => setState(() { formuleSelectionnee = "Premium"; prix = 2000; }),
),
],
),
SizedBox(height: 20),
Text("2. Vos informations", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
TextFormField(
controller: _telephoneController,
keyboardType: TextInputType.phone,
decoration: InputDecoration(labelText: "Numéro Mobile Money (Wave, Orange, MTN...)"),
validator: (v) => v!.isEmpty ? "Obligatoire" : null,
),
TextFormField(
controller: _plaqueController,
decoration: InputDecoration(labelText: "Numéro d'immatriculation du véhicule"),
validator: (v) => v!.isEmpty ? "Obligatoire" : null,
),
SizedBox(height: 30),
Center(
child: ElevatedButton(
style: ElevatedButton.styleFrom(minimumSize: Size(double.infinity, 50)),
onPressed: () {
if (_formKey.currentState!.validate()) _initialiserPaiementMobileMoney();
},
child: Text("Valider et Payer $prix F CFA"),
),
),
SizedBox(height: 30),
Divider(),
Text("Suivi en direct :", style: TextStyle(fontWeight: FontWeight.bold)),
Text(statutCommande, style: TextStyle(fontSize: 16, color: Colors.blue, fontWeight: FontWeight.bold)),
],
),
),
),
);
}
}
// --- 🛠️ 2. INTERFACE EMPLOYÉ / LAVEUR (Réception des missions à domicile) ---
class EcranLaveur extends StatefulWidget {
@override
_EcranLaveurState createState() => _EcranLaveurState();
}
class _EcranLaveurState extends State<EcranLaveur> {
bool estEnLigne = false;
List<Map<String, dynamic>> missionsDisponibles = [];
void _basculerDisponibilite(bool valeur) {
setState(() {
estEnLigne = valeur;
if (estEnLigne) {
// Simulation d'une réception de course par le dispatch algorithmique dans sa zone
missionsDisponibles = [
{"id": 101, "distance": "1.2 km", "quartier": "Près de la pharmacie", "formule": "Premium", "prix": "2000 F", "plaque": "CI-8849-G"}
];
} else {
missionsDisponibles.clear();
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Mon Espace Laveur"),
actions: [
Switch(
value: estEnLigne,
activeColor: Colors.lightGreenAccent,
onChanged: _basculerDisponibilite,
)
],
),
body: Padding(
padding: EdgeInsets.all(20),
child: Column(

Utilisez le code avec précaution.

Get Ouattarayvann’s stories in your inbox

Join Medium for free to get updates from this writer.

crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.between,
children: [
Text(“Mon Statut :”, style: TextStyle(fontSize: 18)),
Text(estEnLigne ? “EN LIGNE (Disponible)” : “HORS LIGNE”,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: estEnLigne ? Colors.green : Colors.red),
),
],
),
SizedBox(height: 30),
Text(“Missions urgentes à proximité :”, style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
SizedBox(height: 10),
Expanded(
child: missionsDisponibles.isEmpty
? Center(child: Text(estEnLigne ? “En attente de commandes dans votre secteur…” : “Mettez-vous en ligne pour recevoir des clients.”))
: ListView.builder(
itemCount: missionsDisponibles.length,
itemBuilder: (context, index) {
final mission = missionsDisponibles[index];
return Card(
elevation: 4,
child: Padding(
padding: EdgeInsets.all(15),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.between,
children: [
Text(“Formule : ${mission[‘formule’]}”, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
Text(mission[‘distance’], style: TextStyle(color: Colors.orange, fontWeight: FontWeight.bold)),
],
),
SizedBox(height: 10),
Text(“Véhicule : ${mission[‘plaque’]}”),
Text(“Indication : ${mission[‘quartier’]}”),
SizedBox(height: 15),
Row(
children: [
Expanded(
child: ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: Colors.green),
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(“Mission acceptée ! Itinéraire GPS ouvert.”)));
},
child: Text(“Accepter la course (${mission[‘prix’]})”),
),
),
],
)
],
),
),
);
},
),
),
],
),
),
);
}
}

--

--