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:
2026-07-17 21:37:06 +02:00
commit 1d3ee69c6d
60 changed files with 4468 additions and 0 deletions
+77
View File
@@ -0,0 +1,77 @@
# Configuration Supabase (à faire une seule fois)
Le backend est partagé par les **deux apps** : `app_fideliter` (tablette / staff)
et `app_fideliter_client` (client). Suivez ces étapes dans l'ordre.
## 1. Créer le projet Supabase
1. Aller sur https://supabase.com → **New project** (gratuit).
2. Choisir un nom, un mot de passe de base de données, une région proche
(ex : `Europe (Paris)`), puis **Create**.
## 2. Créer les tables
1. Dans le projet : menu **SQL Editor****New query**.
2. Copier-coller **tout** le contenu de [`schema.sql`](schema.sql) → **Run**.
3. Vérifier dans **Table Editor** que `clients`, `mouvements`, `recompenses`,
`reglages`, `staff` existent.
## 3. Récupérer les 2 clés à mettre dans les apps
Menu **Project Settings****API** :
- **Project URL** (ex : `https://abcd1234.supabase.co`)
- **anon public** key (une longue chaîne — c'est la clé *publique*, sans danger
dans une app ; la sécurité est assurée par la RLS du `schema.sql`).
Coller ces 2 valeurs dans **chaque** app, fichier `lib/supabase_config.dart` :
```dart
class SupabaseConfig {
static const String url = 'https://VOTRE-PROJET.supabase.co';
static const String anonKey = 'VOTRE_CLE_ANON';
}
```
> ⚠️ Ne jamais mettre la clé **service_role** dans une app : elle contourne
> toute la sécurité. Seule la clé **anon** va dans les apps.
## 4. Créer le compte du magasin (staff)
Le staff (la tablette) a besoin d'un compte avec droits d'écriture.
1. Menu **Authentication****Users****Add user****Create new user**.
Mettre un email + mot de passe (ce sera l'identifiant de la tablette).
Cocher « Auto Confirm User ».
2. Copier l'**UID** de cet utilisateur (colonne `UID`).
3. Menu **SQL Editor**, exécuter (en remplaçant l'UID) :
```sql
insert into public.staff (user_id) values ('COLLER_UID_ICI');
```
Ce compte peut maintenant se connecter sur l'app **tablette** et tout gérer.
Les **clients**, eux, créent leur compte tout seuls depuis l'app client (ils ne
sont pas staff → ils ne peuvent que consulter leurs propres points).
## 5. (Recommandé pour tester vite) désactiver la confirmation d'email
Pour que la création de compte client marche sans étape email pendant les tests :
**Authentication** → **Providers** → **Email** → désactiver
« Confirm email » (à réactiver plus tard en production si souhaité).
## 6. Lancer
```bash
# App tablette (magasin)
cd app_fideliter && flutter run
# App client
cd app_fideliter_client && flutter run
```
- Sur la **tablette** : se connecter avec le compte staff de l'étape 4.
- Sur l'**app client** : « Créer un compte » → un client + son QR sont générés.
- Scanner ce QR depuis la tablette → sa fiche s'ouvre, on ajoute une facture,
ses points se mettent à jour dans l'app client (au rafraîchissement / retour
dans l'app). 🎉
+226
View File
@@ -0,0 +1,226 @@
-- ============================================================================
-- FIDÉLITÉ — Schéma Supabase (Postgres)
-- À coller dans Supabase → SQL Editor → New query → Run.
-- Idempotent : on peut le relancer sans casser l'existant.
-- ============================================================================
-- ----------------------------------------------------------------------------
-- 1. Fonction de génération de code (utilisée en défaut de la table clients,
-- donc définie AVANT la table).
-- ----------------------------------------------------------------------------
-- Génère un code client unique du type « FID-3F9K2A » (sans I, O, 0, 1).
create or replace function public.generer_code_client()
returns text
language plpgsql
set search_path = public
as $$
declare
chars text := 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
nouveau_code text;
i int;
begin
loop
nouveau_code := 'FID-';
for i in 1..6 loop
nouveau_code := nouveau_code ||
substr(chars, floor(random() * length(chars))::int + 1, 1);
end loop;
-- variable renommée pour éviter l'ambiguïté avec la colonne clients.code
exit when not exists
(select 1 from public.clients c where c.code = nouveau_code);
end loop;
return nouveau_code;
end;
$$;
-- ----------------------------------------------------------------------------
-- 2. Tables
-- ----------------------------------------------------------------------------
-- Personnel du magasin (droits d'écriture). On y ajoute l'ID d'un compte auth
-- pour qu'il devienne « staff » (voir SETUP.md).
create table if not exists public.staff (
user_id uuid primary key references auth.users(id) on delete cascade,
created_at timestamptz not null default now()
);
-- Un client fidélité. Lié à un compte de connexion (auth.users) quand le client
-- s'inscrit lui-même depuis l'app client. user_id peut être null si le staff
-- crée un client « au comptoir » sans compte.
create table if not exists public.clients (
id uuid primary key default gen_random_uuid(),
user_id uuid unique references auth.users(id) on delete set null,
code text unique not null default public.generer_code_client(),
nom text not null,
prenom text,
telephone text,
points numeric not null default 0,
created_at timestamptz not null default now()
);
-- Ajout de la colonne prénom si la table existait déjà (migration).
alter table public.clients add column if not exists prenom text;
-- Historique : gain (facture) / dépense (récompense) / ajustement.
create table if not exists public.mouvements (
id uuid primary key default gen_random_uuid(),
client_id uuid not null references public.clients(id) on delete cascade,
type text not null check (type in ('facture','recompense','ajustement')),
montant_euros numeric,
points numeric not null default 0,
libelle text not null default '',
created_at timestamptz not null default now()
);
create index if not exists idx_mouvements_client on public.mouvements(client_id);
-- Catalogue des récompenses.
create table if not exists public.recompenses (
id uuid primary key default gen_random_uuid(),
nom text not null,
cout_points numeric not null default 0,
actif boolean not null default true,
created_at timestamptz not null default now()
);
-- Réglages globaux du magasin (une seule ligne, id = 1).
create table if not exists public.reglages (
id int primary key default 1 check (id = 1),
euros_par_point numeric not null default 10,
nom_magasin text not null default ''
);
insert into public.reglages (id) values (1) on conflict (id) do nothing;
-- ----------------------------------------------------------------------------
-- 3. Fonctions & triggers de logique métier
-- ----------------------------------------------------------------------------
-- Vrai si l'utilisateur connecté fait partie du personnel.
-- SECURITY DEFINER pour pouvoir lire la table staff malgré la RLS.
create or replace function public.est_staff()
returns boolean
language sql
security definer
stable
set search_path = public
as $$
select exists (select 1 from public.staff s where s.user_id = auth.uid());
$$;
-- Tient à jour le solde de points du client à chaque mouvement, pour que le
-- solde reste fiable quelle que soit l'app qui écrit.
create or replace function public.maj_points()
returns trigger
language plpgsql
security definer
set search_path = public
as $$
begin
if (tg_op = 'INSERT') then
update public.clients set points = points + new.points where id = new.client_id;
elsif (tg_op = 'UPDATE') then
update public.clients set points = points - old.points + new.points
where id = new.client_id;
elsif (tg_op = 'DELETE') then
update public.clients set points = points - old.points where id = old.client_id;
end if;
return null;
end;
$$;
drop trigger if exists trg_maj_points on public.mouvements;
create trigger trg_maj_points
after insert or update or delete on public.mouvements
for each row execute function public.maj_points();
-- Empêche un client (non-staff) de modifier son solde, son code ou son user_id.
create or replace function public.proteger_client()
returns trigger
language plpgsql
security definer
set search_path = public
as $$
begin
if not public.est_staff() then
new.points := old.points;
new.code := old.code;
new.user_id := old.user_id;
end if;
return new;
end;
$$;
drop trigger if exists trg_proteger_client on public.clients;
create trigger trg_proteger_client
before update on public.clients
for each row execute function public.proteger_client();
-- ----------------------------------------------------------------------------
-- 4. Sécurité (Row Level Security)
-- Client = lit uniquement SES données. Staff = accès total.
-- ----------------------------------------------------------------------------
alter table public.staff enable row level security;
alter table public.clients enable row level security;
alter table public.mouvements enable row level security;
alter table public.recompenses enable row level security;
alter table public.reglages enable row level security;
-- staff : seul le staff peut lire la liste (personne ne s'auto-déclare staff).
drop policy if exists staff_select on public.staff;
create policy staff_select on public.staff
for select using (public.est_staff());
-- clients
drop policy if exists clients_select on public.clients;
create policy clients_select on public.clients
for select using (public.est_staff() or user_id = auth.uid());
drop policy if exists clients_insert on public.clients;
create policy clients_insert on public.clients
for insert with check (public.est_staff() or user_id = auth.uid());
drop policy if exists clients_update on public.clients;
create policy clients_update on public.clients
for update using (public.est_staff() or user_id = auth.uid());
drop policy if exists clients_delete on public.clients;
create policy clients_delete on public.clients
for delete using (public.est_staff());
-- mouvements : le client lit les siens ; seul le staff écrit (anti-triche).
drop policy if exists mouvements_select on public.mouvements;
create policy mouvements_select on public.mouvements
for select using (
public.est_staff()
or client_id in (select id from public.clients where user_id = auth.uid())
);
drop policy if exists mouvements_insert on public.mouvements;
create policy mouvements_insert on public.mouvements
for insert with check (public.est_staff());
drop policy if exists mouvements_update on public.mouvements;
create policy mouvements_update on public.mouvements
for update using (public.est_staff()) with check (public.est_staff());
drop policy if exists mouvements_delete on public.mouvements;
create policy mouvements_delete on public.mouvements
for delete using (public.est_staff());
-- recompenses : tout compte connecté lit ; seul le staff modifie.
drop policy if exists recompenses_select on public.recompenses;
create policy recompenses_select on public.recompenses
for select using (auth.role() = 'authenticated');
drop policy if exists recompenses_write on public.recompenses;
create policy recompenses_write on public.recompenses
for all using (public.est_staff()) with check (public.est_staff());
-- reglages : tout compte connecté lit ; seul le staff modifie.
drop policy if exists reglages_select on public.reglages;
create policy reglages_select on public.reglages
for select using (auth.role() = 'authenticated');
drop policy if exists reglages_update on public.reglages;
create policy reglages_update on public.reglages
for update using (public.est_staff()) with check (public.est_staff());