1d3ee69c6d
App Flutter de programme de fidélité : login staff, clients (nom/prénom), scan QR, factures (€→points), récompenses, réglages. Backend Supabase (schéma SQL + RLS anti-triche dans supabase/). Icône incluse. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
191 lines
6.1 KiB
Dart
191 lines
6.1 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import '../models/client.dart';
|
|
import '../services/client_repository.dart';
|
|
import '../theme.dart';
|
|
import '../widgets/pastille_points.dart';
|
|
import 'client_detail_screen.dart';
|
|
import 'client_edit_screen.dart';
|
|
|
|
/// Onglet 1 : liste de tous les clients, avec recherche et ajout.
|
|
class ClientsScreen extends StatefulWidget {
|
|
final VoidCallback onAllerScanner;
|
|
const ClientsScreen({super.key, required this.onAllerScanner});
|
|
|
|
@override
|
|
State<ClientsScreen> createState() => _ClientsScreenState();
|
|
}
|
|
|
|
class _ClientsScreenState extends State<ClientsScreen> {
|
|
final _repo = ClientRepository.instance;
|
|
final _rechercheCtrl = TextEditingController();
|
|
String _requete = '';
|
|
|
|
@override
|
|
void dispose() {
|
|
_rechercheCtrl.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _ajouter() async {
|
|
final client = await Navigator.of(context).push<Client>(
|
|
MaterialPageRoute(builder: (_) => const ClientEditScreen()),
|
|
);
|
|
if (client != null && mounted) {
|
|
// On ouvre directement la fiche du nouveau client.
|
|
Navigator.of(context).push(
|
|
MaterialPageRoute(builder: (_) => ClientDetailScreen(clientId: client.id)),
|
|
);
|
|
}
|
|
}
|
|
|
|
void _ouvrir(Client c) {
|
|
Navigator.of(context).push(
|
|
MaterialPageRoute(builder: (_) => ClientDetailScreen(clientId: c.id)),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(title: const Text('Clients')),
|
|
floatingActionButton: FloatingActionButton.extended(
|
|
onPressed: _ajouter,
|
|
icon: const Icon(Icons.person_add_alt_1),
|
|
label: const Text('Nouveau client'),
|
|
),
|
|
body: Column(
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.fromLTRB(16, 4, 16, 12),
|
|
child: TextField(
|
|
controller: _rechercheCtrl,
|
|
onChanged: (v) => setState(() => _requete = v),
|
|
textInputAction: TextInputAction.search,
|
|
decoration: InputDecoration(
|
|
hintText: 'Rechercher (nom, téléphone, code)',
|
|
prefixIcon: const Icon(Icons.search),
|
|
suffixIcon: _requete.isEmpty
|
|
? null
|
|
: IconButton(
|
|
icon: const Icon(Icons.close),
|
|
onPressed: () {
|
|
_rechercheCtrl.clear();
|
|
setState(() => _requete = '');
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Expanded(
|
|
child: AnimatedBuilder(
|
|
animation: _repo,
|
|
builder: (context, _) {
|
|
final liste = _repo.rechercher(_requete);
|
|
if (_repo.clients.isEmpty) return _vide();
|
|
if (liste.isEmpty) {
|
|
return const Center(
|
|
child: Text('Aucun client ne correspond.',
|
|
style: TextStyle(color: AppTheme.grisTexte)),
|
|
);
|
|
}
|
|
return ListView.separated(
|
|
padding: const EdgeInsets.fromLTRB(16, 0, 16, 96),
|
|
itemCount: liste.length,
|
|
separatorBuilder: (_, _) => const SizedBox(height: 10),
|
|
itemBuilder: (context, i) => _carte(liste[i]),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _carte(Client c) {
|
|
return Card(
|
|
child: InkWell(
|
|
borderRadius: BorderRadius.circular(16),
|
|
onTap: () => _ouvrir(c),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(14),
|
|
child: Row(
|
|
children: [
|
|
CircleAvatar(
|
|
radius: 24,
|
|
backgroundColor: AppTheme.accent.withValues(alpha: 0.14),
|
|
child: Text(
|
|
c.initiales,
|
|
style: const TextStyle(
|
|
color: AppTheme.accent, fontWeight: FontWeight.w700),
|
|
),
|
|
),
|
|
const SizedBox(width: 14),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
c.nomComplet,
|
|
style: const TextStyle(
|
|
fontSize: 16, fontWeight: FontWeight.w700),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
c.telephone?.isNotEmpty == true ? c.telephone! : c.code,
|
|
style: const TextStyle(
|
|
fontSize: 13, color: AppTheme.grisTexte),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
PastillePoints(c.points),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _vide() {
|
|
return Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(32),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const Icon(Icons.people_outline, size: 64, color: AppTheme.grisTexte),
|
|
const SizedBox(height: 16),
|
|
const Text(
|
|
'Aucun client pour l\'instant',
|
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w700),
|
|
),
|
|
const SizedBox(height: 8),
|
|
const Text(
|
|
'Créez une fiche client : un QR code de fidélité sera généré automatiquement.',
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(color: AppTheme.grisTexte),
|
|
),
|
|
const SizedBox(height: 24),
|
|
FilledButton.icon(
|
|
onPressed: _ajouter,
|
|
icon: const Icon(Icons.person_add_alt_1),
|
|
label: const Text('Créer le premier client'),
|
|
),
|
|
const SizedBox(height: 12),
|
|
TextButton.icon(
|
|
onPressed: widget.onAllerScanner,
|
|
icon: const Icon(Icons.qr_code_scanner),
|
|
label: const Text('Ou scanner un QR client'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|