initial commit

This commit is contained in:
bastien
2026-06-28 12:29:06 +02:00
parent 79d5b012a9
commit 20f383dd62
93 changed files with 4269 additions and 1 deletions
@@ -0,0 +1,58 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/api/api_client.dart';
import '../../../core/api/api_endpoints.dart';
import '../domain/establishment.dart';
/// Dépôt de données pour les établissements et machines.
class EstablishmentRepository {
EstablishmentRepository(this._apiClient);
final ApiClient _apiClient;
Future<List<Establishment>> fetchEstablishments() async {
final response = await _apiClient.get(ApiEndpoints.establishments);
final data = _extractList(response.data);
return data
.map((json) => Establishment.fromJson(json as Map<String, dynamic>))
.toList();
}
Future<Establishment> fetchEstablishment(String uuid) async {
final response = await _apiClient.get(ApiEndpoints.establishment(uuid));
final json = _extractObject(response.data);
return Establishment.fromJson(json);
}
List<dynamic> _extractList(dynamic data) {
if (data is List<dynamic>) return data;
if (data is Map<String, dynamic> && data['data'] is List<dynamic>) {
return data['data'] as List<dynamic>;
}
return [];
}
Map<String, dynamic> _extractObject(dynamic data) {
if (data is Map<String, dynamic>) {
if (data['data'] is Map<String, dynamic>) {
return data['data'] as Map<String, dynamic>;
}
return data;
}
throw StateError('Réponse API inattendue');
}
}
final establishmentRepositoryProvider = Provider<EstablishmentRepository>((ref) {
return EstablishmentRepository(ref.watch(apiClientProvider));
});
final establishmentsProvider = FutureProvider<List<Establishment>>((ref) async {
return ref.watch(establishmentRepositoryProvider).fetchEstablishments();
});
final establishmentDetailProvider =
FutureProvider.family<Establishment, String>((ref, uuid) async {
return ref.watch(establishmentRepositoryProvider).fetchEstablishment(uuid);
});
@@ -0,0 +1,110 @@
/// Modèle établissement (laverie).
class Establishment {
const Establishment({
required this.uuid,
required this.name,
required this.address,
this.city,
this.zipCode,
this.latitude,
this.longitude,
this.isActive = true,
this.machines = const [],
});
final String uuid;
final String name;
final String address;
final String? city;
final String? zipCode;
final double? latitude;
final double? longitude;
final bool isActive;
final List<Machine> machines;
String get fullAddress {
final parts = [address, zipCode, city].where((p) => p != null && p.isNotEmpty);
return parts.join(', ');
}
factory Establishment.fromJson(Map<String, dynamic> json) {
final machinesJson = json['machines'] as List<dynamic>? ?? [];
return Establishment(
uuid: json['uuid'] as String,
name: json['name'] as String,
address: json['address'] as String? ?? '',
city: json['city'] as String?,
zipCode: json['zip_code'] as String?,
latitude: (json['latitude'] as num?)?.toDouble(),
longitude: (json['longitude'] as num?)?.toDouble(),
isActive: json['is_active'] as bool? ?? true,
machines: machinesJson
.map((m) => Machine.fromJson(m as Map<String, dynamic>))
.toList(),
);
}
}
/// Modèle machine (lave-linge / sèche-linge).
class Machine {
const Machine({
required this.uuid,
required this.name,
required this.type,
required this.status,
this.qrCode,
});
final String uuid;
final String name;
final String type;
final String status;
final String? qrCode;
bool get isAvailable => status == 'available';
String get typeLabel {
switch (type) {
case 'washer_small':
return 'Lave-linge petit';
case 'washer_large':
return 'Lave-linge grand';
case 'dryer_small':
return 'Sèche-linge petit';
case 'dryer_large':
return 'Sèche-linge grand';
default:
return type;
}
}
String get statusLabel {
switch (status) {
case 'available':
return 'Disponible';
case 'reserved':
return 'Réservée';
case 'running':
return 'En cours';
case 'maintenance':
return 'Maintenance';
case 'offline':
return 'Hors ligne';
case 'error':
return 'Erreur';
default:
return status;
}
}
factory Machine.fromJson(Map<String, dynamic> json) {
return Machine(
uuid: json['uuid'] as String,
name: json['name'] as String,
type: json['type'] as String,
status: json['status'] as String,
qrCode: json['qr_code'] as String?,
);
}
}
@@ -0,0 +1,113 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../establishments/data/establishment_repository.dart';
import '../../establishments/domain/establishment.dart';
/// Écran de détail d'un établissement avec liste des machines.
class EstablishmentDetailScreen extends ConsumerWidget {
const EstablishmentDetailScreen({
super.key,
required this.establishmentUuid,
});
final String establishmentUuid;
@override
Widget build(BuildContext context, WidgetRef ref) {
final establishmentAsync =
ref.watch(establishmentDetailProvider(establishmentUuid));
return Scaffold(
appBar: AppBar(title: const Text('Détail laverie')),
body: establishmentAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, _) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Erreur : $error'),
const SizedBox(height: 12),
ElevatedButton(
onPressed: () =>
ref.invalidate(establishmentDetailProvider(establishmentUuid)),
child: const Text('Réessayer'),
),
],
),
),
data: (establishment) => _EstablishmentBody(establishment: establishment),
),
);
}
}
class _EstablishmentBody extends StatelessWidget {
const _EstablishmentBody({required this.establishment});
final Establishment establishment;
@override
Widget build(BuildContext context) {
return ListView(
padding: const EdgeInsets.all(16),
children: [
Text(
establishment.name,
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 4),
Text(establishment.fullAddress),
const SizedBox(height: 24),
Text(
'Machines (${establishment.machines.length})',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
if (establishment.machines.isEmpty)
const Text('Aucune machine disponible')
else
...establishment.machines.map((machine) => _MachineTile(machine: machine)),
],
);
}
}
class _MachineTile extends StatelessWidget {
const _MachineTile({required this.machine});
final Machine machine;
Color _statusColor(BuildContext context) {
switch (machine.status) {
case 'available':
return Colors.green;
case 'running':
return Colors.blue;
case 'reserved':
return Colors.orange;
case 'maintenance':
case 'offline':
return Colors.red;
default:
return Colors.grey;
}
}
@override
Widget build(BuildContext context) {
return Card(
child: ListTile(
leading: Icon(
machine.type.startsWith('washer') ? Icons.water_drop : Icons.air,
color: _statusColor(context),
),
title: Text(machine.name),
subtitle: Text('${machine.typeLabel}${machine.statusLabel}'),
trailing: machine.isAvailable
? const Chip(label: Text('Libre'))
: null,
),
);
}
}