backend api, swagger, tooling, frontend skeleton

This commit is contained in:
2026-01-21 22:05:02 +01:00
parent a9d1ad79ca
commit 88624f3bed
107 changed files with 34393 additions and 6 deletions

View File

@@ -0,0 +1,193 @@
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"gitea.maison43.duckdns.org/gilles/matosbox/internal/data/ent"
"gitea.maison43.duckdns.org/gilles/matosbox/internal/data/ent/categorie"
)
type categorieRequest struct {
Nom *string `json:"nom"`
ParentID *string `json:"parent_id"`
Slug *string `json:"slug"`
Icone *string `json:"icone"`
}
// @Summary Lister les categories
// @Tags Categories
// @Produce json
// @Param page query int false "Page"
// @Param limit query int false "Limite"
// @Success 200 {object} map[string]any
// @Failure 500 {object} map[string]string
// @Router /categories [get]
func (h *Handler) ListCategories(c *gin.Context) {
limit, offset, page := parsePagination(c.Query("page"), c.Query("limit"))
query := h.client.Categorie.Query()
total, err := query.Count(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"erreur": "impossible de compter les categories"})
return
}
items, err := query.
Order(ent.Desc(categorie.FieldCreatedAt)).
Limit(limit).
Offset(offset).
All(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"erreur": "impossible de lister les categories"})
return
}
respondPaginated(c, items, total, page, limit)
}
// @Summary Creer une categorie
// @Tags Categories
// @Accept json
// @Produce json
// @Param body body categorieRequest true "Categorie a creer"
// @Success 201 {object} ent.Categorie
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /categories [post]
func (h *Handler) CreateCategorie(c *gin.Context) {
var req categorieRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"erreur": "donnees invalides"})
return
}
if req.Nom == nil || *req.Nom == "" {
c.JSON(http.StatusBadRequest, gin.H{"erreur": "le champ nom est obligatoire"})
return
}
create := h.client.Categorie.Create().SetNom(*req.Nom)
if req.ParentID != nil && *req.ParentID != "" {
if parsed, err := uuid.Parse(*req.ParentID); err == nil {
create.SetParentID(parsed)
}
}
if req.Slug != nil {
create.SetNillableSlug(req.Slug)
}
if req.Icone != nil {
create.SetNillableIcone(req.Icone)
}
created, err := create.Save(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"erreur": "impossible de creer la categorie"})
return
}
c.JSON(http.StatusCreated, created)
}
// @Summary Recuperer une categorie
// @Tags Categories
// @Produce json
// @Param id path string true "ID categorie"
// @Success 200 {object} ent.Categorie
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /categories/{id} [get]
func (h *Handler) GetCategorie(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"erreur": "identifiant invalide"})
return
}
item, err := h.client.Categorie.Get(c.Request.Context(), id)
if err != nil {
if ent.IsNotFound(err) {
c.JSON(http.StatusNotFound, gin.H{"erreur": "categorie introuvable"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"erreur": "impossible de charger la categorie"})
return
}
c.JSON(http.StatusOK, item)
}
// @Summary Mettre a jour une categorie
// @Tags Categories
// @Accept json
// @Produce json
// @Param id path string true "ID categorie"
// @Param body body categorieRequest true "Categorie a mettre a jour"
// @Success 200 {object} ent.Categorie
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /categories/{id} [put]
func (h *Handler) UpdateCategorie(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"erreur": "identifiant invalide"})
return
}
var req categorieRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"erreur": "donnees invalides"})
return
}
update := h.client.Categorie.UpdateOneID(id)
if req.Nom != nil {
update.SetNom(*req.Nom)
}
if req.ParentID != nil {
if *req.ParentID == "" {
update.ClearParentID()
} else if parsed, err := uuid.Parse(*req.ParentID); err == nil {
update.SetParentID(parsed)
}
}
if req.Slug != nil {
update.SetNillableSlug(req.Slug)
}
if req.Icone != nil {
update.SetNillableIcone(req.Icone)
}
updated, err := update.Save(c.Request.Context())
if err != nil {
if ent.IsNotFound(err) {
c.JSON(http.StatusNotFound, gin.H{"erreur": "categorie introuvable"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"erreur": "impossible de mettre a jour la categorie"})
return
}
c.JSON(http.StatusOK, updated)
}
// @Summary Supprimer une categorie
// @Tags Categories
// @Param id path string true "ID categorie"
// @Success 204 {string} string "No Content"
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /categories/{id} [delete]
func (h *Handler) DeleteCategorie(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"erreur": "identifiant invalide"})
return
}
if err := h.client.Categorie.DeleteOneID(id).Exec(c.Request.Context()); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"erreur": "impossible de supprimer la categorie"})
return
}
c.Status(http.StatusNoContent)
}
// helper for compile reference
var _ = ent.IsNotFound

View File

@@ -0,0 +1,207 @@
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"gitea.maison43.duckdns.org/gilles/matosbox/internal/data/ent"
"gitea.maison43.duckdns.org/gilles/matosbox/internal/data/ent/champpersonnalise"
)
type champPersonnaliseRequest struct {
NomChamp *string `json:"nom_champ"`
TypeChamp *string `json:"type_champ"`
Valeur *string `json:"valeur"`
Unite *string `json:"unite"`
}
// @Summary Lister les champs personnalises
// @Tags ChampsPersonnalises
// @Produce json
// @Param id path string true "ID objet"
// @Param page query int false "Page"
// @Param limit query int false "Limite"
// @Success 200 {object} map[string]any
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /objets/{id}/champs_personnalises [get]
func (h *Handler) ListChampsPersonnalises(c *gin.Context) {
objetID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"erreur": "identifiant invalide"})
return
}
limit, offset, page := parsePagination(c.Query("page"), c.Query("limit"))
query := h.client.ChampPersonnalise.Query().
Where(champpersonnalise.ObjetID(objetID))
total, err := query.Count(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"erreur": "impossible de compter les champs"})
return
}
items, err := query.
Order(ent.Desc(champpersonnalise.FieldCreatedAt)).
Limit(limit).
Offset(offset).
All(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"erreur": "impossible de lister les champs"})
return
}
respondPaginated(c, items, total, page, limit)
}
// @Summary Creer un champ personnalise
// @Tags ChampsPersonnalises
// @Accept json
// @Produce json
// @Param id path string true "ID objet"
// @Param body body champPersonnaliseRequest true "Champ personnalise"
// @Success 201 {object} ent.ChampPersonnalise
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /objets/{id}/champs_personnalises [post]
func (h *Handler) CreateChampPersonnalise(c *gin.Context) {
objetID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"erreur": "identifiant invalide"})
return
}
var req champPersonnaliseRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"erreur": "donnees invalides"})
return
}
if req.NomChamp == nil || *req.NomChamp == "" {
c.JSON(http.StatusBadRequest, gin.H{"erreur": "le champ nom_champ est obligatoire"})
return
}
create := h.client.ChampPersonnalise.Create().
SetObjetID(objetID).
SetNomChamp(*req.NomChamp)
if req.TypeChamp != nil && *req.TypeChamp != "" {
parsed, ok := parseTypeChamp(*req.TypeChamp)
if !ok {
c.JSON(http.StatusBadRequest, gin.H{"erreur": "type_champ invalide"})
return
}
create.SetTypeChamp(parsed)
}
if req.Valeur != nil {
create.SetNillableValeur(req.Valeur)
}
if req.Unite != nil {
create.SetNillableUnite(req.Unite)
}
created, err := create.Save(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"erreur": "impossible de creer le champ"})
return
}
c.JSON(http.StatusCreated, created)
}
// @Summary Mettre a jour un champ personnalise
// @Tags ChampsPersonnalises
// @Accept json
// @Produce json
// @Param id path string true "ID champ"
// @Param body body champPersonnaliseRequest true "Champ personnalise"
// @Success 200 {object} ent.ChampPersonnalise
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /champs_personnalises/{id} [put]
func (h *Handler) UpdateChampPersonnalise(c *gin.Context) {
champID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"erreur": "identifiant invalide"})
return
}
var req champPersonnaliseRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"erreur": "donnees invalides"})
return
}
update := h.client.ChampPersonnalise.UpdateOneID(champID)
if req.NomChamp != nil {
update.SetNomChamp(*req.NomChamp)
}
if req.TypeChamp != nil {
if *req.TypeChamp != "" {
parsed, ok := parseTypeChamp(*req.TypeChamp)
if !ok {
c.JSON(http.StatusBadRequest, gin.H{"erreur": "type_champ invalide"})
return
}
update.SetTypeChamp(parsed)
}
}
if req.Valeur != nil {
update.SetNillableValeur(req.Valeur)
}
if req.Unite != nil {
update.SetNillableUnite(req.Unite)
}
updated, err := update.Save(c.Request.Context())
if err != nil {
if ent.IsNotFound(err) {
c.JSON(http.StatusNotFound, gin.H{"erreur": "champ introuvable"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"erreur": "impossible de mettre a jour le champ"})
return
}
c.JSON(http.StatusOK, updated)
}
// @Summary Supprimer un champ personnalise
// @Tags ChampsPersonnalises
// @Param id path string true "ID champ"
// @Success 204 {string} string "No Content"
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /champs_personnalises/{id} [delete]
func (h *Handler) DeleteChampPersonnalise(c *gin.Context) {
champID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"erreur": "identifiant invalide"})
return
}
if err := h.client.ChampPersonnalise.DeleteOneID(champID).Exec(c.Request.Context()); err != nil {
if ent.IsNotFound(err) {
c.JSON(http.StatusNotFound, gin.H{"erreur": "champ introuvable"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"erreur": "impossible de supprimer le champ"})
return
}
c.Status(http.StatusNoContent)
}
func parseTypeChamp(value string) (champpersonnalise.TypeChamp, bool) {
switch value {
case "string":
return champpersonnalise.TypeChampString, true
case "int":
return champpersonnalise.TypeChampInt, true
case "bool":
return champpersonnalise.TypeChampBool, true
case "date":
return champpersonnalise.TypeChampDate, true
default:
return "", false
}
}

View File

@@ -0,0 +1,214 @@
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"gitea.maison43.duckdns.org/gilles/matosbox/internal/data/ent"
"gitea.maison43.duckdns.org/gilles/matosbox/internal/data/ent/emplacement"
)
type emplacementRequest struct {
Nom *string `json:"nom"`
ParentID *string `json:"parent_id"`
Slug *string `json:"slug"`
Piece *string `json:"piece"`
Meuble *string `json:"meuble"`
NumeroBoite *string `json:"numero_boite"`
Icone *string `json:"icone"`
}
// @Summary Lister les emplacements
// @Tags Emplacements
// @Produce json
// @Param page query int false "Page"
// @Param limit query int false "Limite"
// @Success 200 {object} map[string]any
// @Failure 500 {object} map[string]string
// @Router /emplacements [get]
func (h *Handler) ListEmplacements(c *gin.Context) {
limit, offset, page := parsePagination(c.Query("page"), c.Query("limit"))
query := h.client.Emplacement.Query()
total, err := query.Count(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"erreur": "impossible de compter les emplacements"})
return
}
items, err := query.
Order(ent.Desc(emplacement.FieldCreatedAt)).
Limit(limit).
Offset(offset).
All(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"erreur": "impossible de lister les emplacements"})
return
}
respondPaginated(c, items, total, page, limit)
}
// @Summary Creer un emplacement
// @Tags Emplacements
// @Accept json
// @Produce json
// @Param body body emplacementRequest true "Emplacement a creer"
// @Success 201 {object} ent.Emplacement
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /emplacements [post]
func (h *Handler) CreateEmplacement(c *gin.Context) {
var req emplacementRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"erreur": "donnees invalides"})
return
}
if req.Nom == nil || *req.Nom == "" {
c.JSON(http.StatusBadRequest, gin.H{"erreur": "le champ nom est obligatoire"})
return
}
create := h.client.Emplacement.Create().SetNom(*req.Nom)
if req.ParentID != nil && *req.ParentID != "" {
if parsed, err := uuid.Parse(*req.ParentID); err == nil {
create.SetParentID(parsed)
}
}
if req.Slug != nil {
create.SetNillableSlug(req.Slug)
}
if req.Piece != nil {
create.SetNillablePiece(req.Piece)
}
if req.Meuble != nil {
create.SetNillableMeuble(req.Meuble)
}
if req.NumeroBoite != nil {
create.SetNillableNumeroBoite(req.NumeroBoite)
}
if req.Icone != nil {
create.SetNillableIcone(req.Icone)
}
created, err := create.Save(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"erreur": "impossible de creer l'emplacement"})
return
}
c.JSON(http.StatusCreated, created)
}
// @Summary Recuperer un emplacement
// @Tags Emplacements
// @Produce json
// @Param id path string true "ID emplacement"
// @Success 200 {object} ent.Emplacement
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /emplacements/{id} [get]
func (h *Handler) GetEmplacement(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"erreur": "identifiant invalide"})
return
}
item, err := h.client.Emplacement.Get(c.Request.Context(), id)
if err != nil {
if ent.IsNotFound(err) {
c.JSON(http.StatusNotFound, gin.H{"erreur": "emplacement introuvable"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"erreur": "impossible de charger l'emplacement"})
return
}
c.JSON(http.StatusOK, item)
}
// @Summary Mettre a jour un emplacement
// @Tags Emplacements
// @Accept json
// @Produce json
// @Param id path string true "ID emplacement"
// @Param body body emplacementRequest true "Emplacement a mettre a jour"
// @Success 200 {object} ent.Emplacement
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /emplacements/{id} [put]
func (h *Handler) UpdateEmplacement(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"erreur": "identifiant invalide"})
return
}
var req emplacementRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"erreur": "donnees invalides"})
return
}
update := h.client.Emplacement.UpdateOneID(id)
if req.Nom != nil {
update.SetNom(*req.Nom)
}
if req.ParentID != nil {
if *req.ParentID == "" {
update.ClearParentID()
} else if parsed, err := uuid.Parse(*req.ParentID); err == nil {
update.SetParentID(parsed)
}
}
if req.Slug != nil {
update.SetNillableSlug(req.Slug)
}
if req.Piece != nil {
update.SetNillablePiece(req.Piece)
}
if req.Meuble != nil {
update.SetNillableMeuble(req.Meuble)
}
if req.NumeroBoite != nil {
update.SetNillableNumeroBoite(req.NumeroBoite)
}
if req.Icone != nil {
update.SetNillableIcone(req.Icone)
}
updated, err := update.Save(c.Request.Context())
if err != nil {
if ent.IsNotFound(err) {
c.JSON(http.StatusNotFound, gin.H{"erreur": "emplacement introuvable"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"erreur": "impossible de mettre a jour l'emplacement"})
return
}
c.JSON(http.StatusOK, updated)
}
// @Summary Supprimer un emplacement
// @Tags Emplacements
// @Param id path string true "ID emplacement"
// @Success 204 {string} string "No Content"
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /emplacements/{id} [delete]
func (h *Handler) DeleteEmplacement(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"erreur": "identifiant invalide"})
return
}
if err := h.client.Emplacement.DeleteOneID(id).Exec(c.Request.Context()); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"erreur": "impossible de supprimer l'emplacement"})
return
}
c.Status(http.StatusNoContent)
}
// helper for compile reference
var _ = ent.IsNotFound

View File

@@ -0,0 +1,8 @@
package handlers
import "gitea.maison43.duckdns.org/gilles/matosbox/internal/data/ent"
// Handler regroupe les dependances pour les handlers.
type Handler struct {
client *ent.Client
}

View File

@@ -0,0 +1,108 @@
package handlers
import (
"os"
"strconv"
"strings"
"time"
"gitea.maison43.duckdns.org/gilles/matosbox/internal/data/ent/objet"
)
// parseDateTime parse une date ISO-8601.
func parseDateTime(value string) (time.Time, error) {
return time.Parse(time.RFC3339, value)
}
// resolveCategorie determine la categorie d'une piece jointe.
func resolveCategorie(ext string, mime string) string {
ext = strings.ToLower(ext)
mime = strings.ToLower(mime)
if ext == ".md" || mime == "text/markdown" {
return "markdown_tuto"
}
if mime == "application/pdf" || ext == ".pdf" {
return "pdf_notice"
}
if strings.HasPrefix(mime, "image/") {
return "image"
}
return "image"
}
// maxUploadBytes retourne la taille max d'un fichier en octets.
func maxUploadBytes() int64 {
const defaultMB = 50
value := os.Getenv("MAX_UPLOAD_MB")
if value == "" {
return defaultMB * 1024 * 1024
}
parsed, err := strconv.Atoi(value)
if err != nil || parsed <= 0 {
return defaultMB * 1024 * 1024
}
return int64(parsed) * 1024 * 1024
}
// parsePagination calcule la limite et l'offset a partir des params page/limit.
func parsePagination(pageValue string, limitValue string) (int, int, int) {
const (
defaultLimit = 50
maxLimit = 200
)
page := 1
if pageValue != "" {
if parsed, err := strconv.Atoi(pageValue); err == nil && parsed > 0 {
page = parsed
}
}
limit := defaultLimit
if limitValue != "" {
if parsed, err := strconv.Atoi(limitValue); err == nil && parsed > 0 {
limit = parsed
}
}
if limit > maxLimit {
limit = maxLimit
}
offset := (page - 1) * limit
return limit, offset, page
}
// isAllowedUpload valide le type de fichier (images, PDF, Markdown).
func isAllowedUpload(mime string, ext string) bool {
mime = strings.ToLower(mime)
ext = strings.ToLower(ext)
if strings.HasPrefix(mime, "image/") {
return true
}
if mime == "application/pdf" || ext == ".pdf" {
return true
}
if mime == "text/markdown" || ext == ".md" || ext == ".markdown" {
return true
}
if mime == "text/plain" && (ext == ".md" || ext == ".markdown") {
return true
}
return false
}
// parseStatut convertit un statut texte vers l'enum Ent.
func parseStatut(value string) (objet.Statut, bool) {
switch value {
case "en_stock":
return objet.StatutEnStock, true
case "pret":
return objet.StatutPret, true
case "hors_service":
return objet.StatutHorsService, true
case "archive":
return objet.StatutArchive, true
default:
return "", false
}
}

View File

@@ -0,0 +1,74 @@
package handlers
import "testing"
func TestIsAllowedUpload(t *testing.T) {
cases := []struct {
name string
mime string
ext string
want bool
}{
{"image", "image/png", ".png", true},
{"pdf", "application/pdf", ".pdf", true},
{"md", "text/markdown", ".md", true},
{"md_plain", "text/plain", ".md", true},
{"other", "application/zip", ".zip", false},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
if got := isAllowedUpload(c.mime, c.ext); got != c.want {
t.Fatalf("attendu %v, obtenu %v", c.want, got)
}
})
}
}
func TestMaxUploadBytes(t *testing.T) {
t.Setenv("MAX_UPLOAD_MB", "1")
if got := maxUploadBytes(); got != 1*1024*1024 {
t.Fatalf("taille inattendue: %d", got)
}
t.Setenv("MAX_UPLOAD_MB", "0")
if got := maxUploadBytes(); got != 50*1024*1024 {
t.Fatalf("taille par defaut inattendue: %d", got)
}
}
func TestParseTypeChamp(t *testing.T) {
valid := []string{"string", "int", "bool", "date"}
for _, v := range valid {
if _, ok := parseTypeChamp(v); !ok {
t.Fatalf("type_champ invalide: %s", v)
}
}
if _, ok := parseTypeChamp("x"); ok {
t.Fatal("type_champ devrait etre invalide")
}
}
func TestParseLienType(t *testing.T) {
valid := []string{"stocke", "utilise_dans"}
for _, v := range valid {
if _, ok := parseLienType(v); !ok {
t.Fatalf("type lien invalide: %s", v)
}
}
if _, ok := parseLienType("x"); ok {
t.Fatal("type lien devrait etre invalide")
}
}
func TestParseStatut(t *testing.T) {
valid := []string{"en_stock", "pret", "hors_service", "archive"}
for _, v := range valid {
if _, ok := parseStatut(v); !ok {
t.Fatalf("statut invalide: %s", v)
}
}
if _, ok := parseStatut("x"); ok {
t.Fatal("statut devrait etre invalide")
}
}

View File

@@ -0,0 +1,269 @@
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"gitea.maison43.duckdns.org/gilles/matosbox/internal/data/ent"
"gitea.maison43.duckdns.org/gilles/matosbox/internal/data/ent/lienobjetemplacement"
)
type lienObjetEmplacementRequest struct {
EmplacementID *string `json:"emplacement_id"`
Type *string `json:"type"`
}
type lienObjetEmplacementUpdateRequest struct {
EmplacementID *string `json:"emplacement_id"`
Type *string `json:"type"`
}
// @Summary Lister les liens objet/emplacement
// @Tags LiensEmplacements
// @Produce json
// @Param id path string true "ID objet"
// @Param page query int false "Page"
// @Param limit query int false "Limite"
// @Success 200 {object} map[string]any
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /objets/{id}/liens_emplacements [get]
func (h *Handler) ListLiensEmplacements(c *gin.Context) {
objetID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"erreur": "identifiant invalide"})
return
}
limit, offset, page := parsePagination(c.Query("page"), c.Query("limit"))
query := h.client.LienObjetEmplacement.Query().
Where(lienobjetemplacement.ObjetID(objetID))
total, err := query.Count(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"erreur": "impossible de compter les liens"})
return
}
items, err := query.
Order(ent.Desc(lienobjetemplacement.FieldCreatedAt)).
Limit(limit).
Offset(offset).
All(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"erreur": "impossible de lister les liens"})
return
}
respondPaginated(c, items, total, page, limit)
}
// @Summary Creer un lien objet/emplacement
// @Tags LiensEmplacements
// @Accept json
// @Produce json
// @Param id path string true "ID objet"
// @Param body body lienObjetEmplacementRequest true "Lien"
// @Success 201 {object} ent.LienObjetEmplacement
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 409 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /objets/{id}/liens_emplacements [post]
func (h *Handler) CreateLienEmplacement(c *gin.Context) {
objetID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"erreur": "identifiant invalide"})
return
}
var req lienObjetEmplacementRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"erreur": "donnees invalides"})
return
}
if req.EmplacementID == nil || *req.EmplacementID == "" {
c.JSON(http.StatusBadRequest, gin.H{"erreur": "emplacement_id obligatoire"})
return
}
emplacementID, err := uuid.Parse(*req.EmplacementID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"erreur": "emplacement_id invalide"})
return
}
if _, err := h.client.Emplacement.Get(c.Request.Context(), emplacementID); err != nil {
if ent.IsNotFound(err) {
c.JSON(http.StatusNotFound, gin.H{"erreur": "emplacement introuvable"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"erreur": "impossible de charger l'emplacement"})
return
}
create := h.client.LienObjetEmplacement.Create().
SetObjetID(objetID).
SetEmplacementID(emplacementID)
typeValeur := lienobjetemplacement.TypeStocke
if req.Type != nil && *req.Type != "" {
parsed, ok := parseLienType(*req.Type)
if !ok {
c.JSON(http.StatusBadRequest, gin.H{"erreur": "type invalide"})
return
}
typeValeur = parsed
}
create.SetType(typeValeur)
exists, err := h.client.LienObjetEmplacement.Query().
Where(
lienobjetemplacement.ObjetID(objetID),
lienobjetemplacement.EmplacementID(emplacementID),
lienobjetemplacement.TypeEQ(typeValeur),
).
Exist(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"erreur": "impossible de verifier le lien"})
return
}
if exists {
c.JSON(http.StatusConflict, gin.H{"erreur": "lien deja existant"})
return
}
created, err := create.Save(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"erreur": "impossible de creer le lien"})
return
}
c.JSON(http.StatusCreated, created)
}
// @Summary Supprimer un lien objet/emplacement
// @Tags LiensEmplacements
// @Param id path string true "ID lien"
// @Success 204 {string} string "No Content"
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /liens_emplacements/{id} [delete]
func (h *Handler) DeleteLienEmplacement(c *gin.Context) {
lienID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"erreur": "identifiant invalide"})
return
}
if err := h.client.LienObjetEmplacement.DeleteOneID(lienID).Exec(c.Request.Context()); err != nil {
if ent.IsNotFound(err) {
c.JSON(http.StatusNotFound, gin.H{"erreur": "lien introuvable"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"erreur": "impossible de supprimer le lien"})
return
}
c.Status(http.StatusNoContent)
}
// @Summary Mettre a jour un lien objet/emplacement
// @Tags LiensEmplacements
// @Accept json
// @Produce json
// @Param id path string true "ID lien"
// @Param body body lienObjetEmplacementUpdateRequest true "Mise a jour"
// @Success 200 {object} ent.LienObjetEmplacement
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 409 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /liens_emplacements/{id} [put]
func (h *Handler) UpdateLienEmplacement(c *gin.Context) {
lienID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"erreur": "identifiant invalide"})
return
}
var req lienObjetEmplacementUpdateRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"erreur": "donnees invalides"})
return
}
existant, err := h.client.LienObjetEmplacement.Get(c.Request.Context(), lienID)
if err != nil {
if ent.IsNotFound(err) {
c.JSON(http.StatusNotFound, gin.H{"erreur": "lien introuvable"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"erreur": "impossible de charger le lien"})
return
}
nouveauEmplacementID := existant.EmplacementID
if req.EmplacementID != nil && *req.EmplacementID != "" {
parsed, err := uuid.Parse(*req.EmplacementID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"erreur": "emplacement_id invalide"})
return
}
if _, err := h.client.Emplacement.Get(c.Request.Context(), parsed); err != nil {
if ent.IsNotFound(err) {
c.JSON(http.StatusNotFound, gin.H{"erreur": "emplacement introuvable"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"erreur": "impossible de charger l'emplacement"})
return
}
nouveauEmplacementID = parsed
}
nouveauType := existant.Type
if req.Type != nil && *req.Type != "" {
parsed, ok := parseLienType(*req.Type)
if !ok {
c.JSON(http.StatusBadRequest, gin.H{"erreur": "type invalide"})
return
}
nouveauType = parsed
}
dupe, err := h.client.LienObjetEmplacement.Query().
Where(
lienobjetemplacement.ObjetID(existant.ObjetID),
lienobjetemplacement.EmplacementID(nouveauEmplacementID),
lienobjetemplacement.TypeEQ(nouveauType),
lienobjetemplacement.IDNEQ(lienID),
).
Exist(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"erreur": "impossible de verifier le lien"})
return
}
if dupe {
c.JSON(http.StatusConflict, gin.H{"erreur": "lien deja existant"})
return
}
updated, err := h.client.LienObjetEmplacement.UpdateOneID(lienID).
SetEmplacementID(nouveauEmplacementID).
SetType(nouveauType).
Save(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"erreur": "impossible de mettre a jour le lien"})
return
}
c.JSON(http.StatusOK, updated)
}
func parseLienType(value string) (lienobjetemplacement.Type, bool) {
switch value {
case "stocke":
return lienobjetemplacement.TypeStocke, true
case "utilise_dans":
return lienobjetemplacement.TypeUtiliseDans, true
default:
return "", false
}
}

View File

@@ -0,0 +1,267 @@
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"gitea.maison43.duckdns.org/gilles/matosbox/internal/data/ent"
"gitea.maison43.duckdns.org/gilles/matosbox/internal/data/ent/objet"
)
type objetRequest struct {
Nom *string `json:"nom"`
Description *string `json:"description"`
Quantite *int `json:"quantite"`
PrixAchat *float64 `json:"prix_achat"`
DateAchat *string `json:"date_achat"`
Boutique *string `json:"boutique"`
NumeroSerie *string `json:"numero_serie"`
NumeroModele *string `json:"numero_modele"`
Fabricant *string `json:"fabricant"`
Statut *string `json:"statut"`
Caracteristiques map[string]any `json:"caracteristiques"`
}
// @Summary Lister les objets
// @Tags Objets
// @Produce json
// @Param page query int false "Page"
// @Param limit query int false "Limite"
// @Param statut query string false "Statut"
// @Param nom query string false "Recherche nom"
// @Success 200 {object} map[string]any
// @Failure 500 {object} map[string]string
// @Router /objets [get]
func (h *Handler) ListObjets(c *gin.Context) {
ctx := c.Request.Context()
limit, offset, page := parsePagination(c.Query("page"), c.Query("limit"))
query := h.client.Objet.Query()
if statutValue := c.Query("statut"); statutValue != "" {
statut, ok := parseStatut(statutValue)
if !ok {
c.JSON(http.StatusBadRequest, gin.H{"erreur": "statut invalide"})
return
}
query = query.Where(objet.StatutEQ(statut))
}
if nomValue := c.Query("nom"); nomValue != "" {
query = query.Where(objet.NomContainsFold(nomValue))
}
total, err := query.Count(ctx)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"erreur": "impossible de compter les objets"})
return
}
objets, err := query.
Order(ent.Desc(objet.FieldCreatedAt)).
Limit(limit).
Offset(offset).
All(ctx)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"erreur": "impossible de lister les objets"})
return
}
respondPaginated(c, objets, total, page, limit)
}
// @Summary Creer un objet
// @Tags Objets
// @Accept json
// @Produce json
// @Param body body objetRequest true "Objet a creer"
// @Success 201 {object} ent.Objet
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /objets [post]
func (h *Handler) CreateObjet(c *gin.Context) {
var req objetRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"erreur": "donnees invalides"})
return
}
if req.Nom == nil || *req.Nom == "" {
c.JSON(http.StatusBadRequest, gin.H{"erreur": "le champ nom est obligatoire"})
return
}
create := h.client.Objet.Create().SetNom(*req.Nom)
if req.Description != nil {
create.SetNillableDescription(req.Description)
}
if req.Quantite != nil {
create.SetQuantite(*req.Quantite)
}
if req.PrixAchat != nil {
create.SetNillablePrixAchat(req.PrixAchat)
}
if req.DateAchat != nil {
if parsed, err := parseDateTime(*req.DateAchat); err == nil {
create.SetNillableDateAchat(&parsed)
}
}
if req.Boutique != nil {
create.SetNillableBoutique(req.Boutique)
}
if req.NumeroSerie != nil {
create.SetNillableNumeroSerie(req.NumeroSerie)
}
if req.NumeroModele != nil {
create.SetNillableNumeroModele(req.NumeroModele)
}
if req.Fabricant != nil {
create.SetNillableFabricant(req.Fabricant)
}
if req.Statut != nil {
parsed, ok := parseStatut(*req.Statut)
if !ok {
c.JSON(http.StatusBadRequest, gin.H{"erreur": "statut invalide"})
return
}
create.SetStatut(parsed)
}
if req.Caracteristiques != nil {
create.SetCaracteristiques(req.Caracteristiques)
}
created, err := create.Save(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"erreur": "impossible de creer l'objet"})
return
}
c.JSON(http.StatusCreated, created)
}
// @Summary Recuperer un objet
// @Tags Objets
// @Produce json
// @Param id path string true "ID objet"
// @Success 200 {object} ent.Objet
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /objets/{id} [get]
func (h *Handler) GetObjet(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"erreur": "identifiant invalide"})
return
}
item, err := h.client.Objet.Get(c.Request.Context(), id)
if err != nil {
if ent.IsNotFound(err) {
c.JSON(http.StatusNotFound, gin.H{"erreur": "objet introuvable"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"erreur": "impossible de charger l'objet"})
return
}
c.JSON(http.StatusOK, item)
}
// @Summary Mettre a jour un objet
// @Tags Objets
// @Accept json
// @Produce json
// @Param id path string true "ID objet"
// @Param body body objetRequest true "Objet a mettre a jour"
// @Success 200 {object} ent.Objet
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /objets/{id} [put]
func (h *Handler) UpdateObjet(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"erreur": "identifiant invalide"})
return
}
var req objetRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"erreur": "donnees invalides"})
return
}
update := h.client.Objet.UpdateOneID(id)
if req.Nom != nil {
update.SetNom(*req.Nom)
}
if req.Description != nil {
update.SetNillableDescription(req.Description)
}
if req.Quantite != nil {
update.SetQuantite(*req.Quantite)
}
if req.PrixAchat != nil {
update.SetNillablePrixAchat(req.PrixAchat)
}
if req.DateAchat != nil {
if parsed, err := parseDateTime(*req.DateAchat); err == nil {
update.SetNillableDateAchat(&parsed)
}
}
if req.Boutique != nil {
update.SetNillableBoutique(req.Boutique)
}
if req.NumeroSerie != nil {
update.SetNillableNumeroSerie(req.NumeroSerie)
}
if req.NumeroModele != nil {
update.SetNillableNumeroModele(req.NumeroModele)
}
if req.Fabricant != nil {
update.SetNillableFabricant(req.Fabricant)
}
if req.Statut != nil {
parsed, ok := parseStatut(*req.Statut)
if !ok {
c.JSON(http.StatusBadRequest, gin.H{"erreur": "statut invalide"})
return
}
update.SetStatut(parsed)
}
if req.Caracteristiques != nil {
update.SetCaracteristiques(req.Caracteristiques)
}
updated, err := update.Save(c.Request.Context())
if err != nil {
if ent.IsNotFound(err) {
c.JSON(http.StatusNotFound, gin.H{"erreur": "objet introuvable"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"erreur": "impossible de mettre a jour l'objet"})
return
}
c.JSON(http.StatusOK, updated)
}
// @Summary Supprimer un objet
// @Tags Objets
// @Param id path string true "ID objet"
// @Success 204 {string} string "No Content"
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /objets/{id} [delete]
func (h *Handler) DeleteObjet(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"erreur": "identifiant invalide"})
return
}
if err := h.client.Objet.DeleteOneID(id).Exec(c.Request.Context()); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"erreur": "impossible de supprimer l'objet"})
return
}
c.Status(http.StatusNoContent)
}
// helper for compile reference
var _ = ent.IsNotFound

View File

@@ -0,0 +1,25 @@
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
)
type paginatedResponse struct {
Items any `json:"items"`
Meta struct {
Total int `json:"total"`
Page int `json:"page"`
Limit int `json:"limit"`
} `json:"meta"`
}
func respondPaginated(c *gin.Context, items any, total int, page int, limit int) {
var resp paginatedResponse
resp.Items = items
resp.Meta.Total = total
resp.Meta.Page = page
resp.Meta.Limit = limit
c.JSON(http.StatusOK, resp)
}

View File

@@ -0,0 +1,275 @@
package handlers
import (
"errors"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"gitea.maison43.duckdns.org/gilles/matosbox/internal/data/ent"
"gitea.maison43.duckdns.org/gilles/matosbox/internal/data/ent/piecejointe"
)
// UploadPiecesJointes gere l'upload multiple des fichiers d'un objet.
// @Summary Uploader des pieces jointes
// @Tags PiecesJointes
// @Accept multipart/form-data
// @Produce json
// @Param id path string true "ID objet"
// @Param fichiers formData file true "Fichiers (multi)"
// @Success 201 {object} map[string]any
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 413 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /objets/{id}/pieces_jointes [post]
func (h *Handler) UploadPiecesJointes(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"erreur": "identifiant invalide"})
return
}
if _, err := h.client.Objet.Get(c.Request.Context(), id); err != nil {
if ent.IsNotFound(err) {
c.JSON(http.StatusNotFound, gin.H{"erreur": "objet introuvable"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"erreur": "impossible de charger l'objet"})
return
}
form, err := c.MultipartForm()
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"erreur": "formulaire invalide"})
return
}
files := form.File["fichiers"]
if len(files) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"erreur": "aucun fichier fourni"})
return
}
maxBytes := maxUploadBytes()
hasPrincipale, err := h.client.PieceJointe.Query().
Where(
piecejointe.ObjetID(id),
piecejointe.EstPrincipale(true),
).
Exist(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"erreur": "impossible de verifier la piece jointe principale"})
return
}
baseDir := os.Getenv("ATTACHMENTS_DIR")
if baseDir == "" {
baseDir = "./data/pieces_jointes"
}
if err := os.MkdirAll(baseDir, 0o755); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"erreur": "impossible de creer le dossier de stockage"})
return
}
var saved []map[string]any
for idx, file := range files {
ext := strings.ToLower(filepath.Ext(file.Filename))
if file.Size > maxBytes {
c.JSON(http.StatusRequestEntityTooLarge, gin.H{"erreur": "fichier trop volumineux"})
return
}
mime := file.Header.Get("Content-Type")
if !isAllowedUpload(mime, ext) {
c.JSON(http.StatusBadRequest, gin.H{"erreur": "type de fichier non supporte"})
return
}
uuidName := uuid.New().String() + ext
path := filepath.Join(baseDir, uuidName)
if err := c.SaveUploadedFile(file, path); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"erreur": "echec enregistrement fichier"})
return
}
categorie := resolveCategorie(ext, mime)
estPrincipale := idx == 0 && !hasPrincipale
pj, err := h.client.PieceJointe.Create().
SetObjetID(id).
SetNomFichier(file.Filename).
SetChemin(path).
SetTypeMime(mime).
SetCategorie(piecejointe.Categorie(categorie)).
SetEstPrincipale(estPrincipale).
Save(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"erreur": "impossible d'enregistrer la piece jointe"})
return
}
saved = append(saved, map[string]any{
"id": pj.ID,
"nom_fichier": pj.NomFichier,
"chemin": pj.Chemin,
"type_mime": pj.TypeMime,
"categorie": pj.Categorie,
"est_principale": pj.EstPrincipale,
})
}
c.JSON(http.StatusCreated, gin.H{"pieces_jointes": saved})
}
// ListPiecesJointes retourne la liste des pieces jointes d'un objet.
// @Summary Lister les pieces jointes
// @Tags PiecesJointes
// @Produce json
// @Param id path string true "ID objet"
// @Param page query int false "Page"
// @Param limit query int false "Limite"
// @Success 200 {object} map[string]any
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /objets/{id}/pieces_jointes [get]
func (h *Handler) ListPiecesJointes(c *gin.Context) {
objetID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"erreur": "identifiant invalide"})
return
}
limit, offset, page := parsePagination(c.Query("page"), c.Query("limit"))
query := h.client.PieceJointe.Query().
Where(piecejointe.ObjetID(objetID))
total, err := query.Count(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"erreur": "impossible de compter les pieces jointes"})
return
}
items, err := query.
Order(ent.Desc(piecejointe.FieldCreatedAt)).
Limit(limit).
Offset(offset).
All(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"erreur": "impossible de lister les pieces jointes"})
return
}
respondPaginated(c, items, total, page, limit)
}
// DeletePieceJointe supprime une piece jointe et son fichier.
// @Summary Supprimer une piece jointe
// @Tags PiecesJointes
// @Param id path string true "ID piece jointe"
// @Success 204 {string} string "No Content"
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /pieces_jointes/{id} [delete]
func (h *Handler) DeletePieceJointe(c *gin.Context) {
pieceID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"erreur": "identifiant invalide"})
return
}
item, err := h.client.PieceJointe.Get(c.Request.Context(), pieceID)
if err != nil {
if ent.IsNotFound(err) {
c.JSON(http.StatusNotFound, gin.H{"erreur": "piece jointe introuvable"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"erreur": "impossible de charger la piece jointe"})
return
}
if item.Chemin != "" {
if err := os.Remove(item.Chemin); err != nil && !errors.Is(err, os.ErrNotExist) {
c.JSON(http.StatusInternalServerError, gin.H{"erreur": "impossible de supprimer le fichier"})
return
}
}
if err := h.client.PieceJointe.DeleteOneID(pieceID).Exec(c.Request.Context()); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"erreur": "impossible de supprimer la piece jointe"})
return
}
c.Status(http.StatusNoContent)
}
// SetPieceJointePrincipale marque une piece jointe comme principale.
// @Summary Definir la piece jointe principale
// @Tags PiecesJointes
// @Produce json
// @Param id path string true "ID piece jointe"
// @Success 200 {object} ent.PieceJointe
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /pieces_jointes/{id}/principale [put]
func (h *Handler) SetPieceJointePrincipale(c *gin.Context) {
pieceID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"erreur": "identifiant invalide"})
return
}
item, err := h.client.PieceJointe.Get(c.Request.Context(), pieceID)
if err != nil {
if ent.IsNotFound(err) {
c.JSON(http.StatusNotFound, gin.H{"erreur": "piece jointe introuvable"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"erreur": "impossible de charger la piece jointe"})
return
}
tx, err := h.client.Tx(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"erreur": "impossible d'ouvrir la transaction"})
return
}
defer func() {
if err != nil {
_ = tx.Rollback()
}
}()
if _, err = tx.PieceJointe.Update().
Where(piecejointe.ObjetID(item.ObjetID)).
SetEstPrincipale(false).
Save(c.Request.Context()); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"erreur": "impossible de reinitialiser la principale"})
_ = tx.Rollback()
return
}
if _, err = tx.PieceJointe.UpdateOneID(pieceID).
SetEstPrincipale(true).
Save(c.Request.Context()); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"erreur": "impossible de definir la principale"})
_ = tx.Rollback()
return
}
if err = tx.Commit(); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"erreur": "impossible de finaliser la transaction"})
return
}
updated, err := h.client.PieceJointe.Get(c.Request.Context(), pieceID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"erreur": "impossible de charger la piece jointe"})
return
}
c.JSON(http.StatusOK, updated)
}

View File

@@ -0,0 +1,44 @@
package handlers
import (
"github.com/gin-gonic/gin"
"gitea.maison43.duckdns.org/gilles/matosbox/internal/data/ent"
)
// RegisterRoutes enregistre les routes v1.
func RegisterRoutes(r *gin.Engine, client *ent.Client) {
h := &Handler{client: client}
v1 := r.Group("/v1")
{
v1.GET("/objets", h.ListObjets)
v1.POST("/objets", h.CreateObjet)
v1.GET("/objets/:id", h.GetObjet)
v1.PUT("/objets/:id", h.UpdateObjet)
v1.DELETE("/objets/:id", h.DeleteObjet)
v1.POST("/objets/:id/pieces_jointes", h.UploadPiecesJointes)
v1.GET("/objets/:id/pieces_jointes", h.ListPiecesJointes)
v1.DELETE("/pieces_jointes/:id", h.DeletePieceJointe)
v1.PUT("/pieces_jointes/:id/principale", h.SetPieceJointePrincipale)
v1.GET("/objets/:id/champs_personnalises", h.ListChampsPersonnalises)
v1.POST("/objets/:id/champs_personnalises", h.CreateChampPersonnalise)
v1.PUT("/champs_personnalises/:id", h.UpdateChampPersonnalise)
v1.DELETE("/champs_personnalises/:id", h.DeleteChampPersonnalise)
v1.GET("/objets/:id/liens_emplacements", h.ListLiensEmplacements)
v1.POST("/objets/:id/liens_emplacements", h.CreateLienEmplacement)
v1.PUT("/liens_emplacements/:id", h.UpdateLienEmplacement)
v1.DELETE("/liens_emplacements/:id", h.DeleteLienEmplacement)
v1.GET("/categories", h.ListCategories)
v1.POST("/categories", h.CreateCategorie)
v1.GET("/categories/:id", h.GetCategorie)
v1.PUT("/categories/:id", h.UpdateCategorie)
v1.DELETE("/categories/:id", h.DeleteCategorie)
v1.GET("/emplacements", h.ListEmplacements)
v1.POST("/emplacements", h.CreateEmplacement)
v1.GET("/emplacements/:id", h.GetEmplacement)
v1.PUT("/emplacements/:id", h.UpdateEmplacement)
v1.DELETE("/emplacements/:id", h.DeleteEmplacement)
}
}