Merge branch 'release/0.0.2'
This commit is contained in:
74
MIGRATION_API.md
Normal file
74
MIGRATION_API.md
Normal file
@@ -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
|
||||
|
||||
|
||||
3
src/.env
3
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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
136
src/src/lib/apiClient.js
Normal file
136
src/src/lib/apiClient.js
Normal file
@@ -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 };
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
@@ -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,7 +104,7 @@ function DashboardPage() {
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Unexpected error fetching orders:", err);
|
||||
setFetchError("Une erreur inattendue est survenue.");
|
||||
setFetchError(err.message || "Une erreur inattendue est survenue.");
|
||||
toast({
|
||||
title: "Erreur critique",
|
||||
description: "Une erreur inattendue est survenue lors du chargement des commandes.",
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user