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,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). 🎉
|
||||
@@ -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());
|
||||
Reference in New Issue
Block a user