chore: build backend frontend web
This commit is contained in:
@@ -13,6 +13,7 @@ type Category struct {
|
|||||||
Name string `json:"name" gorm:"column:name"`
|
Name string `json:"name" gorm:"column:name"`
|
||||||
Color string `json:"color" gorm:"column:color"`
|
Color string `json:"color" gorm:"column:color"`
|
||||||
IsComingSoon bool `json:"is_coming_soon" gorm:"column:is_coming_soon"`
|
IsComingSoon bool `json:"is_coming_soon" gorm:"column:is_coming_soon"`
|
||||||
|
Position int `json:"position" gorm:"column:position"`
|
||||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,7 +31,7 @@ func ValidateCategoryColor(color string) error {
|
|||||||
|
|
||||||
func (d *Database) GetAllCategories() ([]Category, error) {
|
func (d *Database) GetAllCategories() ([]Category, error) {
|
||||||
var categories []Category
|
var categories []Category
|
||||||
if err := d.GDB.Order("name ASC").Find(&categories).Error; err != nil {
|
if err := d.GDB.Order("position ASC, name ASC").Find(&categories).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if categories == nil {
|
if categories == nil {
|
||||||
@@ -43,7 +44,9 @@ func (d *Database) CreateCategory(name, color string, isComingSoon bool) (*Categ
|
|||||||
if color == "" {
|
if color == "" {
|
||||||
color = "#7c3aed"
|
color = "#7c3aed"
|
||||||
}
|
}
|
||||||
c := Category{Name: name, Color: color, IsComingSoon: isComingSoon}
|
var maxPos int
|
||||||
|
d.GDB.Model(&Category{}).Select("COALESCE(MAX(position), 0)").Scan(&maxPos)
|
||||||
|
c := Category{Name: name, Color: color, IsComingSoon: isComingSoon, Position: maxPos + 1}
|
||||||
if err := d.GDB.Create(&c).Error; err != nil {
|
if err := d.GDB.Create(&c).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -82,6 +85,18 @@ func (d *Database) DeleteCategory(id int) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReorderCategories met à jour les positions selon l'ordre du tableau d'IDs fourni.
|
||||||
|
func (d *Database) ReorderCategories(ids []int) error {
|
||||||
|
tx := d.GDB.Begin()
|
||||||
|
for i, id := range ids {
|
||||||
|
if err := tx.Model(&Category{}).Where("id = ?", id).Update("position", i+1).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tx.Commit().Error
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Database) CategoryExists(name string) (bool, error) {
|
func (d *Database) CategoryExists(name string) (bool, error) {
|
||||||
var count int64
|
var count int64
|
||||||
err := d.GDB.Model(&Category{}).Where("name = ?", name).Count(&count).Error
|
err := d.GDB.Model(&Category{}).Where("name = ?", name).Count(&count).Error
|
||||||
|
|||||||
@@ -165,6 +165,23 @@ func InitDB() *Database {
|
|||||||
log.Fatalf("❌ Erreur migration categories.is_coming_soon: %v", err)
|
log.Fatalf("❌ Erreur migration categories.is_coming_soon: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Migration: position d'affichage des catégories
|
||||||
|
if _, err = database.Exec(`ALTER TABLE categories ADD COLUMN IF NOT EXISTS position INTEGER NOT NULL DEFAULT 0`); err != nil {
|
||||||
|
log.Fatalf("❌ Erreur migration categories.position: %v", err)
|
||||||
|
}
|
||||||
|
// Backfill: attribuer des positions aux catégories existantes (ordre alphabétique)
|
||||||
|
if _, err = database.Exec(`
|
||||||
|
UPDATE categories c
|
||||||
|
SET position = sub.rn
|
||||||
|
FROM (
|
||||||
|
SELECT id, ROW_NUMBER() OVER (ORDER BY name ASC) AS rn
|
||||||
|
FROM categories
|
||||||
|
) sub
|
||||||
|
WHERE c.id = sub.id AND c.position = 0
|
||||||
|
`); err != nil {
|
||||||
|
log.Fatalf("❌ Erreur backfill categories.position: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Migration: table paramètres globaux de l'application
|
// Migration: table paramètres globaux de l'application
|
||||||
if _, err = database.Exec(`CREATE TABLE IF NOT EXISTS app_settings (
|
if _, err = database.Exec(`CREATE TABLE IF NOT EXISTS app_settings (
|
||||||
key VARCHAR(100) PRIMARY KEY,
|
key VARCHAR(100) PRIMARY KEY,
|
||||||
|
|||||||
@@ -109,6 +109,26 @@ func UpdateCategory(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ReorderCategories(c *gin.Context) {
|
||||||
|
database := c.MustGet("database").(*db.Database)
|
||||||
|
|
||||||
|
var req struct {
|
||||||
|
IDs []int `json:"ids" binding:"required"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil || len(req.IDs) == 0 {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Liste d'IDs requise"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := database.ReorderCategories(req.IDs); err != nil {
|
||||||
|
log.Printf("❌ [CATEGORIES] Reorder erreur: %v", err)
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur lors du réordonnancement"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
|
|
||||||
func DeleteCategory(c *gin.Context) {
|
func DeleteCategory(c *gin.Context) {
|
||||||
database := c.MustGet("database").(*db.Database)
|
database := c.MustGet("database").(*db.Database)
|
||||||
|
|
||||||
|
|||||||
@@ -201,6 +201,7 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services
|
|||||||
// CATÉGORIES - GESTION ADMIN
|
// CATÉGORIES - GESTION ADMIN
|
||||||
// ============================================
|
// ============================================
|
||||||
adminGroupV2.POST("/categories", handlers.CreateCategory)
|
adminGroupV2.POST("/categories", handlers.CreateCategory)
|
||||||
|
adminGroupV2.PUT("/categories/reorder", handlers.ReorderCategories)
|
||||||
adminGroupV2.PUT("/categories/:id", handlers.UpdateCategory)
|
adminGroupV2.PUT("/categories/:id", handlers.UpdateCategory)
|
||||||
adminGroupV2.DELETE("/categories/:id", handlers.DeleteCategory)
|
adminGroupV2.DELETE("/categories/:id", handlers.DeleteCategory)
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|||||||
@@ -92,9 +92,6 @@ export const extractUsernameFromToken = (): string | null => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* ✅ Synchroniser sessionStorage avec JWT
|
|
||||||
*/
|
|
||||||
export const syncUsernameFromJWT = (): string | null => {
|
export const syncUsernameFromJWT = (): string | null => {
|
||||||
const jwtUsername = extractUsernameFromToken();
|
const jwtUsername = extractUsernameFromToken();
|
||||||
|
|
||||||
@@ -2198,12 +2195,14 @@ export const getMyPointsRewards = async (): Promise<{
|
|||||||
reward: PointsRewardConfig | null;
|
reward: PointsRewardConfig | null;
|
||||||
}> => {
|
}> => {
|
||||||
const token = getAuthToken();
|
const token = getAuthToken();
|
||||||
if (!token) return { success: false, enabled: false, pools: [], reward: null };
|
if (!token)
|
||||||
|
return { success: false, enabled: false, pools: [], reward: null };
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_URL}/points/rewards`, {
|
const response = await fetch(`${API_URL}/points/rewards`, {
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
});
|
});
|
||||||
if (!response.ok) return { success: false, enabled: false, pools: [], reward: null };
|
if (!response.ok)
|
||||||
|
return { success: false, enabled: false, pools: [], reward: null };
|
||||||
const data = await safeJson(response);
|
const data = await safeJson(response);
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -2259,7 +2258,9 @@ export const getOrderRatingStatus = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const claimMyReward = async (poolKey: string): Promise<{
|
export const claimMyReward = async (
|
||||||
|
poolKey: string,
|
||||||
|
): Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
description?: string;
|
description?: string;
|
||||||
remaining_rewards?: number;
|
remaining_rewards?: number;
|
||||||
@@ -2279,7 +2280,8 @@ export const claimMyReward = async (poolKey: string): Promise<{
|
|||||||
body: JSON.stringify({ pool_key: poolKey }),
|
body: JSON.stringify({ pool_key: poolKey }),
|
||||||
});
|
});
|
||||||
const data = await safeJson(response);
|
const data = await safeJson(response);
|
||||||
if (!response.ok) return { success: false, error: data.error || "Erreur" };
|
if (!response.ok)
|
||||||
|
return { success: false, error: data.error || "Erreur" };
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
description: data.description,
|
description: data.description,
|
||||||
|
|||||||
Reference in New Issue
Block a user