Initial commit — app fidélité magasin (tablette)
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>
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
/// Un client fidélité. Le [code] est ce qui est encodé dans son QR code :
|
||||
/// scanner le QR permet de retrouver le compte.
|
||||
class Client {
|
||||
final String id; // uuid Supabase
|
||||
|
||||
/// Code unique du compte (encodé dans le QR), ex : « FID-3F9K2A ».
|
||||
final String code;
|
||||
|
||||
final String nom;
|
||||
final String? prenom;
|
||||
final String? telephone;
|
||||
|
||||
/// Solde de points courant (peut être fractionnaire, ex : 12,48).
|
||||
final double points;
|
||||
|
||||
const Client({
|
||||
required this.id,
|
||||
required this.code,
|
||||
required this.nom,
|
||||
this.prenom,
|
||||
this.telephone,
|
||||
this.points = 0,
|
||||
});
|
||||
|
||||
/// Nom complet affiché : « Prénom Nom » (ou juste le nom si pas de prénom).
|
||||
String get nomComplet {
|
||||
final p = (prenom ?? '').trim();
|
||||
return p.isEmpty ? nom : '$p $nom';
|
||||
}
|
||||
|
||||
/// Initiales pour l'avatar (ex : « Jean Dupont » → « JD »).
|
||||
String get initiales {
|
||||
final p = (prenom ?? '').trim();
|
||||
final n = nom.trim();
|
||||
final a = p.isNotEmpty ? p[0] : (n.isNotEmpty ? n[0] : '?');
|
||||
final b = n.isNotEmpty ? n[0] : '';
|
||||
return (a + b).toUpperCase();
|
||||
}
|
||||
|
||||
Client copyWith({
|
||||
String? id,
|
||||
String? code,
|
||||
String? nom,
|
||||
String? prenom,
|
||||
String? telephone,
|
||||
double? points,
|
||||
}) {
|
||||
return Client(
|
||||
id: id ?? this.id,
|
||||
code: code ?? this.code,
|
||||
nom: nom ?? this.nom,
|
||||
prenom: prenom ?? this.prenom,
|
||||
telephone: telephone ?? this.telephone,
|
||||
points: points ?? this.points,
|
||||
);
|
||||
}
|
||||
|
||||
factory Client.fromMap(Map<String, dynamic> map) {
|
||||
return Client(
|
||||
id: map['id'].toString(),
|
||||
code: (map['code'] as String?) ?? '',
|
||||
nom: (map['nom'] as String?) ?? '',
|
||||
prenom: map['prenom'] as String?,
|
||||
telephone: map['telephone'] as String?,
|
||||
points: (map['points'] as num?)?.toDouble() ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/// Type d'un mouvement de points dans l'historique d'un client.
|
||||
class TypeMouvement {
|
||||
static const facture = 'facture'; // gain de points suite à un achat
|
||||
static const recompense = 'recompense'; // dépense de points sur une récompense
|
||||
static const ajustement = 'ajustement'; // correction manuelle (+/-)
|
||||
}
|
||||
|
||||
/// Une ligne d'historique : gain (facture) ou dépense (récompense) de points
|
||||
/// pour un client donné.
|
||||
class Mouvement {
|
||||
final String id;
|
||||
final String clientId;
|
||||
|
||||
/// Voir [TypeMouvement].
|
||||
final String type;
|
||||
|
||||
/// Montant de la facture en euros (uniquement pour [TypeMouvement.facture]).
|
||||
final double? montantEuros;
|
||||
|
||||
/// Points crédités (positif) ou débités (négatif).
|
||||
final double points;
|
||||
|
||||
/// Libellé lisible, ex : « Facture 25,00 € » ou « Café offert ».
|
||||
final String libelle;
|
||||
|
||||
/// Date du mouvement.
|
||||
final DateTime date;
|
||||
|
||||
const Mouvement({
|
||||
required this.id,
|
||||
required this.clientId,
|
||||
required this.type,
|
||||
this.montantEuros,
|
||||
required this.points,
|
||||
required this.libelle,
|
||||
required this.date,
|
||||
});
|
||||
|
||||
bool get estGain => points >= 0;
|
||||
|
||||
factory Mouvement.fromMap(Map<String, dynamic> map) {
|
||||
return Mouvement(
|
||||
id: map['id'].toString(),
|
||||
clientId: map['client_id'].toString(),
|
||||
type: (map['type'] as String?) ?? TypeMouvement.ajustement,
|
||||
montantEuros: (map['montant_euros'] as num?)?.toDouble(),
|
||||
points: (map['points'] as num?)?.toDouble() ?? 0,
|
||||
libelle: (map['libelle'] as String?) ?? '',
|
||||
date: DateTime.tryParse(map['created_at']?.toString() ?? '')?.toLocal() ??
|
||||
DateTime.fromMillisecondsSinceEpoch(0),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/// Une récompense du catalogue : ce qu'un client peut obtenir en échange de
|
||||
/// ses points (ex : « Café offert » = 10 pts).
|
||||
class Recompense {
|
||||
final String id;
|
||||
final String nom;
|
||||
|
||||
/// Coût en points pour obtenir la récompense.
|
||||
final double coutPoints;
|
||||
|
||||
/// Récompense proposée (true) ou masquée sans être supprimée (false).
|
||||
final bool actif;
|
||||
|
||||
const Recompense({
|
||||
required this.id,
|
||||
required this.nom,
|
||||
required this.coutPoints,
|
||||
this.actif = true,
|
||||
});
|
||||
|
||||
Recompense copyWith({
|
||||
String? id,
|
||||
String? nom,
|
||||
double? coutPoints,
|
||||
bool? actif,
|
||||
}) {
|
||||
return Recompense(
|
||||
id: id ?? this.id,
|
||||
nom: nom ?? this.nom,
|
||||
coutPoints: coutPoints ?? this.coutPoints,
|
||||
actif: actif ?? this.actif,
|
||||
);
|
||||
}
|
||||
|
||||
factory Recompense.fromMap(Map<String, dynamic> map) {
|
||||
return Recompense(
|
||||
id: map['id'].toString(),
|
||||
nom: (map['nom'] as String?) ?? '',
|
||||
coutPoints: (map['cout_points'] as num?)?.toDouble() ?? 0,
|
||||
actif: (map['actif'] as bool?) ?? true,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user