push 03082025

This commit is contained in:
andrymodeste 2025-08-03 18:36:34 +02:00
parent c2cd893835
commit c9f4a0ce45
5 changed files with 330 additions and 286 deletions

View File

@ -16,6 +16,7 @@ class CommandeDetail {
final DateTime? dateService; final DateTime? dateService;
final DateTime createdAt; final DateTime createdAt;
final DateTime updatedAt; final DateTime updatedAt;
final String tablename;
final List<CommandeItem> items; final List<CommandeItem> items;
CommandeDetail({ CommandeDetail({
@ -36,6 +37,7 @@ class CommandeDetail {
required this.createdAt, required this.createdAt,
required this.updatedAt, required this.updatedAt,
required this.items, required this.items,
required this.tablename,
}); });
factory CommandeDetail.fromJson(Map<String, dynamic> json) { factory CommandeDetail.fromJson(Map<String, dynamic> json) {
@ -54,6 +56,7 @@ class CommandeDetail {
totalTtc: double.tryParse(data['total_ttc']?.toString() ?? '0') ?? 0.0, totalTtc: double.tryParse(data['total_ttc']?.toString() ?? '0') ?? 0.0,
modePaiement: data['mode_paiement'], modePaiement: data['mode_paiement'],
commentaires: data['commentaires'], commentaires: data['commentaires'],
tablename: json['tablename'] ?? 'Inconnue',
serveur: data['serveur'] ?? 'Serveur par défaut', serveur: data['serveur'] ?? 'Serveur par défaut',
dateCommande: dateCommande:
data['date_commande'] != null data['date_commande'] != null

View File

@ -10,7 +10,8 @@ class TableOrder {
final double? total; // Optionnel pour les commandes en cours final double? total; // Optionnel pour les commandes en cours
final bool isEncashed; final bool isEncashed;
final String? time; // Heure de la commande si applicable final String? time; // Heure de la commande si applicable
final String? date; // Date de la commande si applicable final DateTime? date; // Date de la commande si applicable
final String? tablename; // Date de la commande si applicable
// final int? persons; // Nombre de personnes si applicable // final int? persons; // Nombre de personnes si applicable
TableOrder({ TableOrder({
@ -25,31 +26,34 @@ class TableOrder {
this.isEncashed = false, this.isEncashed = false,
this.time, this.time,
this.date, this.date,
this.tablename,
// this.persons, // this.persons,
}); });
factory TableOrder.fromJson(Map<String, dynamic> json) {
return TableOrder(
id: json['id'] ?? 0,
nom: json['nom'] ?? '',
capacity: json['capacity'] ?? 1,
status: json['statut'] ?? 'available',
location: json['location'] ?? '',
tablename: json['tablename'] ?? '',
createdAt: json['created_at'] != null
? DateTime.parse(json['created_at'])
: DateTime.now(),
updatedAt: json['updated_at'] != null
? DateTime.parse(json['updated_at'])
: DateTime.now(),
total: json['total_ht'] != null
? double.tryParse(json['total_ht'].toString())
: null,
isEncashed: json['is_encashed'] ?? false,
time: json['time'],
date: json['date_commande'] != null
? DateTime.parse(json['date_commande']) // tu avais mis updated_at ici par erreur
: null,
);
}
factory TableOrder.fromJson(Map<String, dynamic> json) {
return TableOrder(
id: json['id'] ?? 0,
nom: json['nom'] ?? '',
capacity: json['capacity'] ?? 1,
status: json['status'] ?? 'available',
location: json['location'] ?? '',
createdAt:
json['created_at'] != null
? DateTime.parse(json['created_at'])
: DateTime.now(),
updatedAt:
json['updated_at'] != null
? DateTime.parse(json['updated_at'])
: DateTime.now(),
total: json['total'] != null ? (json['total'] as num).toDouble() : null,
isEncashed: json['is_encashed'] ?? false,
time: json['time'],
date: json['date'],
// persons: json['persons'],
);
}
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return { return {
@ -58,6 +62,7 @@ class TableOrder {
'capacity': capacity, 'capacity': capacity,
'status': status, 'status': status,
'location': location, 'location': location,
'tablename': tablename,
'created_at': createdAt.toIso8601String(), 'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(), 'updated_at': updatedAt.toIso8601String(),
if (total != null) 'total': total, if (total != null) 'total': total,
@ -122,7 +127,7 @@ class TableOrder {
double? total, double? total,
bool? isEncashed, bool? isEncashed,
String? time, String? time,
String? date, DateTime? date,
int? persons, int? persons,
}) { }) {
return TableOrder( return TableOrder(
@ -153,6 +158,10 @@ class TableOrder {
@override @override
int get hashCode => id.hashCode; int get hashCode => id.hashCode;
get items => null;
where(bool Function(dynamic commande) param0) {}
} }
// Énumération pour les statuts (optionnel, pour plus de type safety) // Énumération pour les statuts (optionnel, pour plus de type safety)

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import './tables.dart'; import './tables.dart';
import '../layouts/main_layout.dart'; import '../layouts/main_layout.dart';
@ -54,6 +55,15 @@ class _LoginScreenState extends State<LoginScreen> {
super.dispose(); super.dispose();
} }
// Méthode pour gérer la touche Entrée
void _handleKeyPress(KeyEvent event) {
if (event is KeyDownEvent && event.logicalKey == LogicalKeyboardKey.enter) {
if (!_isLoading) {
_login();
}
}
}
void _login() async { void _login() async {
if (!_formKey.currentState!.validate()) return; if (!_formKey.currentState!.validate()) return;
@ -99,279 +109,295 @@ class _LoginScreenState extends State<LoginScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return KeyboardListener(
backgroundColor: const Color(0xFFF5F5F5), // Light gray background focusNode: FocusNode(),
body: Center( onKeyEvent: _handleKeyPress,
child: SingleChildScrollView( child: Scaffold(
padding: const EdgeInsets.all(24.0), backgroundColor: const Color(0xFFF5F5F5), // Light gray background
child: Card( body: Center(
elevation: 2, child: SingleChildScrollView(
shape: RoundedRectangleBorder( padding: const EdgeInsets.all(24.0),
borderRadius: BorderRadius.circular(8), child: Card(
), elevation: 2,
child: Container( shape: RoundedRectangleBorder(
width: 400, borderRadius: BorderRadius.circular(8),
padding: const EdgeInsets.all(40.0), ),
child: Form( child: Container(
key: _formKey, width: 400,
child: Column( padding: const EdgeInsets.all(40.0),
mainAxisSize: MainAxisSize.min, child: Form(
children: [ key: _formKey,
// Logo personnalisé child: Column(
Container( mainAxisSize: MainAxisSize.min,
width: 80, children: [
height: 80, // Logo personnalisé
child: Image.asset(
'assets/logo_transparent.png',
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) {
// Fallback en cas d'erreur de chargement
return Container(
width: 64,
height: 64,
decoration: const BoxDecoration(
color: Colors.green,
shape: BoxShape.circle,
),
child: const Icon(
Icons.restaurant_menu,
color: Colors.white,
size: 32,
),
);
},
),
),
const SizedBox(height: 24),
// Title
const Text(
'Restaurant App',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
const SizedBox(height: 8),
// Subtitle
const Text(
'Connectez-vous pour accéder au système de commandes',
style: TextStyle(color: Colors.grey, fontSize: 14),
textAlign: TextAlign.center,
),
const SizedBox(height: 32),
// Error message
if (_errorMessage != null)
Container( Container(
width: double.infinity, width: 80,
padding: const EdgeInsets.all(12), height: 80,
margin: const EdgeInsets.only(bottom: 16), child: Image.asset(
decoration: BoxDecoration( 'assets/logo_transparent.png',
color: Colors.red.shade50, fit: BoxFit.contain,
border: Border.all(color: Colors.red.shade200), errorBuilder: (context, error, stackTrace) {
borderRadius: BorderRadius.circular(4), // Fallback en cas d'erreur de chargement
), return Container(
child: Text( width: 64,
_errorMessage!, height: 64,
style: TextStyle( decoration: const BoxDecoration(
color: Colors.red.shade600, color: Colors.green,
fontSize: 14, shape: BoxShape.circle,
), ),
textAlign: TextAlign.center, child: const Icon(
Icons.restaurant_menu,
color: Colors.white,
size: 32,
),
);
},
), ),
), ),
const SizedBox(height: 24),
// Email label // Title
const Align( const Text(
alignment: Alignment.centerLeft, 'Restaurant App',
child: Text(
'Email',
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.black87, color: Colors.black87,
fontWeight: FontWeight.w500,
), ),
), ),
), const SizedBox(height: 8),
const SizedBox(height: 8),
// Email field // Subtitle
TextFormField( const Text(
controller: emailController, 'Connectez-vous pour accéder au système de commandes',
decoration: InputDecoration( style: TextStyle(color: Colors.grey, fontSize: 14),
prefixIcon: const Icon( textAlign: TextAlign.center,
Icons.email_outlined,
color: Colors.grey,
size: 20,
),
hintText: 'serveur@restaurant.com',
hintStyle: const TextStyle(color: Colors.grey),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(4),
borderSide: BorderSide(color: Colors.grey.shade300),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(4),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(4),
borderSide: const BorderSide(color: Colors.green),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 16,
),
), ),
keyboardType: TextInputType.emailAddress, const SizedBox(height: 32),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer votre email';
}
return null;
},
),
const SizedBox(height: 16),
// Password label // Error message
const Align( if (_errorMessage != null)
alignment: Alignment.centerLeft, Container(
child: Text( width: double.infinity,
'Mot de passe', padding: const EdgeInsets.all(12),
style: TextStyle( margin: const EdgeInsets.only(bottom: 16),
fontSize: 14, decoration: BoxDecoration(
color: Colors.black87, color: Colors.red.shade50,
fontWeight: FontWeight.w500, border: Border.all(color: Colors.red.shade200),
),
),
),
const SizedBox(height: 8),
// Password field
TextFormField(
controller: passwordController,
decoration: InputDecoration(
prefixIcon: const Icon(
Icons.lock_outline,
color: Colors.grey,
size: 20,
),
hintText: '••••••••',
hintStyle: const TextStyle(color: Colors.grey),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(4),
borderSide: BorderSide(color: Colors.grey.shade300),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(4),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(4),
borderSide: const BorderSide(color: Colors.green),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 16,
),
),
obscureText: true,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer votre mot de passe';
}
return null;
},
),
const SizedBox(height: 24),
// Login button
SizedBox(
width: double.infinity,
height: 48,
child: ElevatedButton(
onPressed: _isLoading ? null : _login,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
), ),
elevation: 0, child: Text(
), _errorMessage!,
child:
_isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
)
: const Text(
'Se connecter',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
),
const SizedBox(height: 24),
// Demo accounts section
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(4),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Comptes de démonstration :',
style: TextStyle( style: TextStyle(
color: Colors.red.shade600,
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.black87,
), ),
textAlign: TextAlign.center,
), ),
const SizedBox(height: 8), ),
RichText(
text: const TextSpan( // Email label
style: TextStyle( const Align(
fontSize: 13, alignment: Alignment.centerLeft,
color: Colors.grey, child: Text(
height: 1.4, 'Email',
), style: TextStyle(
children: [ fontSize: 14,
TextSpan( color: Colors.black87,
text: 'Serveur : ', fontWeight: FontWeight.w500,
style: TextStyle(fontWeight: FontWeight.w500),
),
TextSpan(
text: 'serveur@restaurant.com / serveur123\n',
),
TextSpan(
text: 'Admin : ',
style: TextStyle(fontWeight: FontWeight.w500),
),
TextSpan(
text: 'admin@restaurant.com / admin123',
),
],
),
), ),
], ),
), ),
), const SizedBox(height: 8),
],
// Email field
TextFormField(
controller: emailController,
decoration: InputDecoration(
prefixIcon: const Icon(
Icons.email_outlined,
color: Colors.grey,
size: 20,
),
hintText: 'serveur@restaurant.com',
hintStyle: const TextStyle(color: Colors.grey),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(4),
borderSide: BorderSide(color: Colors.grey.shade300),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(4),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(4),
borderSide: const BorderSide(color: Colors.green),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 16,
),
),
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer votre email';
}
return null;
},
),
const SizedBox(height: 16),
// Password label
const Align(
alignment: Alignment.centerLeft,
child: Text(
'Mot de passe',
style: TextStyle(
fontSize: 14,
color: Colors.black87,
fontWeight: FontWeight.w500,
),
),
),
const SizedBox(height: 8),
// Password field
TextFormField(
controller: passwordController,
decoration: InputDecoration(
prefixIcon: const Icon(
Icons.lock_outline,
color: Colors.grey,
size: 20,
),
hintText: '••••••••',
hintStyle: const TextStyle(color: Colors.grey),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(4),
borderSide: BorderSide(color: Colors.grey.shade300),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(4),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(4),
borderSide: const BorderSide(color: Colors.green),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 16,
),
),
obscureText: true,
textInputAction: TextInputAction.done,
onFieldSubmitted: (_) => _login(), // Validation quand on appuie sur Entrée
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer votre mot de passe';
}
return null;
},
),
const SizedBox(height: 24),
// Login button
SizedBox(
width: double.infinity,
height: 48,
child: ElevatedButton(
onPressed: _isLoading ? null : _login,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
),
elevation: 0,
),
child:
_isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
)
: const Text(
'Se connecter',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
),
const SizedBox(height: 24),
// Demo accounts section
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(4),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Comptes de démonstration :',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.black87,
),
),
const SizedBox(height: 8),
RichText(
text: const TextSpan(
style: TextStyle(
fontSize: 13,
color: Colors.grey,
height: 1.4,
),
children: [
TextSpan(
text: 'Serveur : ',
style: TextStyle(fontWeight: FontWeight.w500),
),
TextSpan(
text: 'serveur@restaurant.com / serveur123\n',
),
TextSpan(
text: 'Admin : ',
style: TextStyle(fontWeight: FontWeight.w500),
),
TextSpan(
text: 'admin@restaurant.com / admin123',
),
],
),
),
const SizedBox(height: 8),
const Text(
'Astuce : Appuyez sur Entrée pour vous connecter',
style: TextStyle(
fontSize: 12,
color: Colors.grey,
fontStyle: FontStyle.italic,
),
),
],
),
),
],
),
), ),
), ),
), ),
@ -432,4 +458,4 @@ class HomeScreen extends StatelessWidget {
), ),
); );
} }
} }

View File

@ -21,7 +21,7 @@ class RestaurantApiService {
static Future<List<TableOrder>> getCommandes() async { static Future<List<TableOrder>> getCommandes() async {
try { try {
final response = await http final response = await http
.get(Uri.parse('$baseUrl/api/commandes'), headers: _headers) .get(Uri.parse('$baseUrl/api/commandes?statut=servie'), headers: _headers)
.timeout( .timeout(
const Duration(seconds: 30), const Duration(seconds: 30),
onTimeout: () => throw TimeoutException('Délai d\'attente dépassé'), onTimeout: () => throw TimeoutException('Délai d\'attente dépassé'),
@ -171,6 +171,7 @@ class RestaurantApiService {
totalTtc: 14.00, totalTtc: 14.00,
modePaiement: null, modePaiement: null,
commentaires: null, commentaires: null,
tablename: 'a',
serveur: "Serveur par défaut", serveur: "Serveur par défaut",
dateCommande: DateTime.parse("2025-08-02T15:03:44.000Z"), dateCommande: DateTime.parse("2025-08-02T15:03:44.000Z"),
dateService: null, dateService: null,
@ -234,7 +235,6 @@ class RestaurantApiService {
updatedAt: DateTime.now(), updatedAt: DateTime.now(),
total: 27.00, total: 27.00,
time: '00:02', time: '00:02',
date: '02/08/2025',
), ),
// Ajoutez d'autres tables de test... // Ajoutez d'autres tables de test...
]; ];

View File

@ -12,8 +12,13 @@ class CommandeCard extends StatelessWidget {
required this.onAllerCaisse, required this.onAllerCaisse,
}); });
String _formatTime(DateTime dateTime) {
return '${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')} ${dateTime.day.toString().padLeft(2, '0')}/${dateTime.month.toString().padLeft(2, '0')}/${dateTime.year}';
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
margin: EdgeInsets.only(bottom: 16), margin: EdgeInsets.only(bottom: 16),
decoration: BoxDecoration( decoration: BoxDecoration(
@ -38,7 +43,7 @@ class CommandeCard extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text( Text(
'Table ${commande.tableNumber}', ' ${commande.tablename}',
style: TextStyle( style: TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -57,7 +62,7 @@ class CommandeCard extends StatelessWidget {
Icon(Icons.check_circle, color: Colors.white, size: 16), Icon(Icons.check_circle, color: Colors.white, size: 16),
SizedBox(width: 4), SizedBox(width: 4),
Text( Text(
'À encaisser', 'Près à encaisser',
style: TextStyle( style: TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 12, fontSize: 12,
@ -78,7 +83,8 @@ class CommandeCard extends StatelessWidget {
Icon(Icons.access_time, size: 16, color: Colors.grey[600]), Icon(Icons.access_time, size: 16, color: Colors.grey[600]),
SizedBox(width: 6), SizedBox(width: 6),
Text( Text(
'${commande.time}${commande.date} ', // Fixed: Pass DateTime directly, not as string
commande.date != null ? _formatTime(commande.date!) : 'Date non disponible',
style: TextStyle(color: Colors.grey[600], fontSize: 14), style: TextStyle(color: Colors.grey[600], fontSize: 14),
), ),
], ],
@ -99,7 +105,7 @@ class CommandeCard extends StatelessWidget {
style: TextStyle(color: Colors.grey[600], fontSize: 14), style: TextStyle(color: Colors.grey[600], fontSize: 14),
), ),
Text( Text(
'${commande.total?.toStringAsFixed(2)} MGA', '${commande.total?.toStringAsFixed(2) ?? '0.00'} MGA',
style: TextStyle( style: TextStyle(
fontSize: 20, fontSize: 20,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -129,4 +135,4 @@ class CommandeCard extends StatelessWidget {
), ),
); );
} }
} }