diff --git a/MIGRATION_API.md b/MIGRATION_API.md new file mode 100644 index 0000000..4fb53c8 --- /dev/null +++ b/MIGRATION_API.md @@ -0,0 +1,74 @@ +# Migration du Backoffice vers l'API Laravel + +Ce document décrit les changements effectués pour migrer le backoffice de Supabase vers l'API Laravel. + +## Changements effectués + +### 1. Remplacement de Supabase par un client API personnalisé + +- **Supprimé** : `src/lib/supabaseClient.js` +- **Créé** : `src/lib/apiClient.js` - Client API pour communiquer avec l'API Laravel + +### 2. Adaptation de l'authentification + +- **Fichier modifié** : `src/contexts/AuthContext.jsx` +- **Changement** : Remplacement de `supabase.functions.invoke('verify-admin-password')` par `api.auth.login()` + +### 3. Adaptation du dashboard + +- **Fichier modifié** : `src/pages/DashboardPage.jsx` +- **Changements** : + - Remplacement de `supabase.from('orders').select()` par `api.orders.getOrders()` + - Remplacement de `supabase.functions.invoke('update-order-status')` par `api.orders.updateStatus()` + - Utilisation de `api.orders.getMetrics()` pour les métriques au lieu de récupérer toutes les commandes + +### 4. Suppression des dépendances Supabase + +- **Supprimé** : `@supabase/supabase-js` du `package.json` + +## Configuration + +### Variables d'environnement + +Créez un fichier `.env` dans `backoffice/src/` avec : + +```env +# URL de l'API Laravel +# En développement local : http://localhost:8000 +# En production : https://api.ditesleenchanson.fr +VITE_API_URL=http://localhost:8000 +``` + +### Installation + +1. Installer les dépendances (sans Supabase) : +```bash +cd backoffice/src +npm install +``` + +2. Configurer le fichier `.env` avec l'URL de l'API + +3. Démarrer le serveur de développement : +```bash +npm run dev +``` + +## API Endpoints utilisés + +- `POST /auth/admin/login` - Authentification admin +- `GET /orders` - Liste des commandes avec pagination +- `GET /orders/metrics` - Métriques des commandes +- `POST /orders/{orderId}/status` - Mise à jour du statut d'une commande + +## Authentification + +Le token d'authentification est stocké dans `localStorage` sous la clé `admin_auth_token` et est envoyé dans le header `Authorization: Bearer {token}` pour les requêtes protégées. + +## Notes + +- Le format des réponses de l'API a été adapté pour correspondre au format Supabase utilisé précédemment +- Les métriques incluent maintenant les revenus par statut (pendingRevenue, processingRevenue, completedRevenue) +- La pagination fonctionne de la même manière qu'avec Supabase + + diff --git a/src/.env b/src/.env index a7d2746..99bf59d 100644 --- a/src/.env +++ b/src/.env @@ -1,2 +1 @@ -VITE_SUPABASE_URL=https://supabase.abpcode.fr -VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzY2Nzc3OTU5LCJleHAiOjE5MjQ0NTc5NTl9.I-qytVb1ef6QMR8IUDePJzESO3bJAnsGE075XQ2xiaI +VITE_API_URL=https://api.dites-le-en-chanson.fr diff --git a/src/package.json b/src/package.json index 0f09636..f2a5d32 100644 --- a/src/package.json +++ b/src/package.json @@ -1,7 +1,7 @@ { "name": "web-app", "type": "module", - "version": "0.0.1", + "version": "0.0.2", "private": true, "scripts": { "dev": "vite --host :: --port 3000", @@ -20,7 +20,6 @@ "@radix-ui/react-toast": "^1.1.5", "@radix-ui/react-slider": "^1.1.2", "@radix-ui/react-select": "^2.0.0", - "@supabase/supabase-js": "^2.39.0", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", "framer-motion": "^10.16.4", diff --git a/src/src/contexts/AuthContext.jsx b/src/src/contexts/AuthContext.jsx index ad736b2..1740d37 100644 --- a/src/src/contexts/AuthContext.jsx +++ b/src/src/contexts/AuthContext.jsx @@ -1,5 +1,5 @@ import React, { createContext, useContext, useState, useEffect } from 'react'; -import { supabase } from '@/lib/supabaseClient'; +import { api } from '@/lib/apiClient'; const AuthContext = createContext(); @@ -27,18 +27,17 @@ export function AuthProvider({ children }) { const login = async (password) => { setIsLoading(true); try { - const { data, error } = await supabase.functions.invoke('verify-admin-password', { - body: { password }, - }); + const { data, error } = await api.auth.login(password); if (error) { - console.error('Error invoking verify-admin-password function:', error); + console.error('Error during login:', error); setIsLoading(false); return false; } if (data && data.success) { - const token = btoa(Date.now().toString()); // Simple token for client-side + // Utiliser le token retourné par l'API ou créer un token simple + const token = data.token || btoa(Date.now().toString()); localStorage.setItem('admin_auth_token', token); setIsAuthenticated(true); setIsLoading(false); diff --git a/src/src/lib/apiClient.js b/src/src/lib/apiClient.js new file mode 100644 index 0000000..1326cb0 --- /dev/null +++ b/src/src/lib/apiClient.js @@ -0,0 +1,136 @@ +/** + * Client API pour remplacer Supabase + * Communique avec l'API Laravel + */ + +const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000'; + +/** + * Récupère le token d'authentification depuis localStorage + */ +function getAuthToken() { + return localStorage.getItem('admin_auth_token'); +} + +/** + * Effectue une requête HTTP vers l'API + */ +async function apiRequest(endpoint, options = {}) { + const url = `${API_URL}${endpoint}`; + const token = getAuthToken(); + + const headers = { + 'Content-Type': 'application/json', + ...options.headers, + }; + + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + + const config = { + ...options, + headers, + }; + + if (options.body && typeof options.body === 'object') { + config.body = JSON.stringify(options.body); + } + + try { + const response = await fetch(url, config); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({ error: 'Erreur inconnue' })); + throw { + message: errorData.error || `Erreur HTTP ${response.status}`, + status: response.status, + data: errorData, + }; + } + + return await response.json(); + } catch (error) { + if (error.status) { + throw error; + } + throw { + message: error.message || 'Erreur de connexion', + status: 0, + }; + } +} + +/** + * API client pour remplacer Supabase + */ +export const api = { + /** + * Authentification admin + */ + auth: { + async login(password) { + const data = await apiRequest('/auth/admin/login', { + method: 'POST', + body: { password }, + }); + return { data, error: data.success ? null : { message: data.error || 'Erreur de connexion' } }; + }, + }, + + /** + * Commandes + */ + orders: { + /** + * Récupère les commandes avec pagination + */ + async getOrders({ page = 1, perPage = 10, status = null } = {}) { + const params = new URLSearchParams({ + page: page.toString(), + per_page: perPage.toString(), + }); + + if (status) { + params.append('status', status); + } + + const data = await apiRequest(`/orders?${params.toString()}`, { + method: 'GET', + }); + + // Adapter le format pour correspondre à Supabase + return { + data: data.data || [], + error: null, + count: data.total || 0, + }; + }, + + /** + * Récupère les métriques des commandes + */ + async getMetrics() { + const data = await apiRequest('/orders/metrics', { + method: 'GET', + }); + return { data, error: null }; + }, + + /** + * Met à jour le statut d'une commande + */ + async updateStatus(orderId, newStatus) { + try { + const data = await apiRequest(`/orders/${orderId}/status`, { + method: 'POST', + body: { newStatus }, + }); + return { data, error: null }; + } catch (error) { + return { data: null, error }; + } + }, + }, +}; + diff --git a/src/src/lib/supabaseClient.js b/src/src/lib/supabaseClient.js deleted file mode 100644 index 733b874..0000000 --- a/src/src/lib/supabaseClient.js +++ /dev/null @@ -1,6 +0,0 @@ -import { createClient } from '@supabase/supabase-js'; - -const supabaseUrl = import.meta.env.VITE_SUPABASE_URL; -const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY; - -export const supabase = createClient(supabaseUrl, supabaseAnonKey); diff --git a/src/src/pages/DashboardPage.jsx b/src/src/pages/DashboardPage.jsx index 0a8516f..60fa3f3 100644 --- a/src/src/pages/DashboardPage.jsx +++ b/src/src/pages/DashboardPage.jsx @@ -18,7 +18,7 @@ import { import { Button } from '@/components/ui/button'; import { useAuth } from '@/contexts/AuthContext'; import { toast } from '@/components/ui/use-toast'; -import { supabase } from '@/lib/supabaseClient'; +import { api } from '@/lib/apiClient'; import { MetricCard } from '@/components/dashboard/MetricCard'; import { OrderCard } from '@/components/dashboard/OrderCard'; @@ -53,62 +53,44 @@ function DashboardPage() { completedRevenue: 0, }); - const calculateMetrics = useCallback((allOrders) => { - const pendingOrders = allOrders.filter(o => o.status === STATUS_PENDING); - const processingOrders = allOrders.filter(o => o.status === STATUS_PROCESSING); - const completedOrders = allOrders.filter(o => o.status === STATUS_COMPLETED); - - setMetrics({ - total: allOrders.length, - pending: pendingOrders.length, - processing: processingOrders.length, - completed: completedOrders.length, - totalRevenue: allOrders.reduce((sum, order) => sum + (Number(order.price) || 0), 0), - pendingRevenue: pendingOrders.reduce((sum, order) => sum + (Number(order.price) || 0), 0), - processingRevenue: processingOrders.reduce((sum, order) => sum + (Number(order.price) || 0), 0), - completedRevenue: completedOrders.reduce((sum, order) => sum + (Number(order.price) || 0), 0), - }); - }, []); - const fetchOrdersAndMetrics = useCallback(async () => { try { - const { data: allOrdersData, error: metricsError } = await supabase - .from('orders') - .select('status, price'); + const { data: metricsData, error: metricsError } = await api.orders.getMetrics(); if (metricsError) { - console.error("Error fetching all orders for metrics:", metricsError); - } else if (allOrdersData) { - calculateMetrics(allOrdersData); + console.error("Error fetching metrics:", metricsError); + } else if (metricsData) { + setMetrics({ + total: metricsData.total || 0, + pending: metricsData.pending || 0, + processing: metricsData.processing || 0, + completed: metricsData.completed || 0, + totalRevenue: metricsData.totalRevenue || 0, + pendingRevenue: metricsData.pendingRevenue || 0, + processingRevenue: metricsData.processingRevenue || 0, + completedRevenue: metricsData.completedRevenue || 0, + }); } } catch (err) { - console.error("Unexpected error fetching all orders for metrics:", err); + console.error("Unexpected error fetching metrics:", err); } - }, [calculateMetrics]); + }, []); const fetchPaginatedOrders = useCallback(async () => { setIsLoading(true); setFetchError(null); - - const from = (currentPage - 1) * ORDERS_PER_PAGE; - const to = from + ORDERS_PER_PAGE - 1; - - let query = supabase - .from('orders') - .select('*', { count: 'exact' }) - .order('created_at', { ascending: false }) - .range(from, to); - - if (activeFilter && statusMapping[activeFilter]) { - query = query.eq('status', statusMapping[activeFilter]); - } try { - const { data, error, count } = await query; + const status = activeFilter && statusMapping[activeFilter] ? statusMapping[activeFilter] : null; + const { data, error, count } = await api.orders.getOrders({ + page: currentPage, + perPage: ORDERS_PER_PAGE, + status, + }); if (error) { console.error("Error fetching orders:", error); - setFetchError(error.message); + setFetchError(error.message || "Impossible de récupérer les commandes."); toast({ title: "Erreur de chargement", description: "Impossible de récupérer les commandes.", @@ -122,8 +104,8 @@ function DashboardPage() { } } catch (err) { console.error("Unexpected error fetching orders:", err); - setFetchError("Une erreur inattendue est survenue."); - toast({ + setFetchError(err.message || "Une erreur inattendue est survenue."); + toast({ title: "Erreur critique", description: "Une erreur inattendue est survenue lors du chargement des commandes.", variant: "destructive", @@ -147,29 +129,46 @@ function DashboardPage() { } setIsUpdating(orderId); try { - const { data: updatedOrder, error: functionError } = await supabase.functions.invoke('update-order-status', { - body: JSON.stringify({ orderId, newStatus }), - }); + const { data: updatedOrder, error } = await api.orders.updateStatus(orderId, newStatus); - if (functionError) { - console.error('Supabase function error:', functionError); - toast({ title: "Erreur de mise à jour (fonction)", description: `Statut non mis à jour: ${functionError.message}`, variant: "destructive" }); + if (error) { + console.error('API error:', error); + toast({ + title: "Erreur de mise à jour", + description: `Statut non mis à jour: ${error.message || 'Erreur inconnue'}`, + variant: "destructive" + }); } else if (updatedOrder && updatedOrder.error) { - // Handle errors returned by the function logic itself - console.error('Error from Edge Function:', updatedOrder.error); - toast({ title: "Erreur de mise à jour (logique fonction)", description: `Statut non mis à jour: ${updatedOrder.error}`, variant: "destructive" }); + // Handle errors returned by the API + console.error('Error from API:', updatedOrder.error); + toast({ + title: "Erreur de mise à jour", + description: `Statut non mis à jour: ${updatedOrder.error}`, + variant: "destructive" + }); } else if (updatedOrder) { await fetchOrdersAndMetrics(); await fetchPaginatedOrders(); - toast({ title: "Statut mis à jour !", description: `Commande #${orderId.substring(0,8)}... est maintenant "${newStatus}".` }); + toast({ + title: "Statut mis à jour !", + description: `Commande #${orderId.substring(0,8)}... est maintenant "${newStatus}".` + }); } else { - toast({ title: "Mise à jour incertaine", description: "Aucune donnée de confirmation de la fonction.", variant: "destructive" }); + toast({ + title: "Mise à jour incertaine", + description: "Aucune donnée de confirmation de l'API.", + variant: "destructive" + }); await fetchOrdersAndMetrics(); await fetchPaginatedOrders(); } } catch (err) { - console.error("Unexpected error invoking Supabase function:", err); - toast({ title: "Erreur critique", description: "Erreur inattendue lors de l'appel de la fonction de mise à jour.", variant: "destructive" }); + console.error("Unexpected error updating order status:", err); + toast({ + title: "Erreur critique", + description: err.message || "Erreur inattendue lors de la mise à jour du statut.", + variant: "destructive" + }); } finally { setIsUpdating(null); }