5 Commits
0.0.1 ... main

Author SHA1 Message Date
balvarez
fd840f1c69 Merge branch 'release/0.0.2'
All checks were successful
DitesLeEnChanson/backoffice/pipeline/tag This commit looks good
2026-01-03 20:24:00 +01:00
balvarez
7495308d4a version 0.0.2 2026-01-03 20:23:45 +01:00
balvarez
af34f5ff18 adaptation api 2026-01-03 20:23:00 +01:00
balvarez
1a60a0a411 clear project 2026-01-03 19:56:21 +01:00
balvarez
6cb0e2ffea Merge tag '0.0.1' into develop
0.0.1
2026-01-01 13:21:03 +01:00
7 changed files with 272 additions and 72 deletions

74
MIGRATION_API.md Normal file
View 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

View File

@@ -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

View File

@@ -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",

View File

@@ -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
View 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 };
}
},
},
};

View File

@@ -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);

View File

@@ -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);
}