diff --git a/handlers/api.go b/handlers/api.go new file mode 100644 index 0000000..6169ba2 --- /dev/null +++ b/handlers/api.go @@ -0,0 +1,591 @@ +package handlers + +import ( + "MiauInv/models" + "MiauInv/storage" + "database/sql" + "encoding/json" + "log" + "net/http" + "strconv" + "strings" +) + +func Location(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + switch r.Method { + + case http.MethodGet: + idStr := r.URL.Query().Get("id") + + if idStr != "" { + id, err := strconv.Atoi(idStr) + if err != nil { + http.Error(w, "Invalid ID parameter", http.StatusBadRequest) + return + } + + var loc models.Location + err = storage.DB.QueryRow("SELECT id, name FROM locations WHERE id = ?", id).Scan(&loc.ID, &loc.Name) + if err != nil { + if err == sql.ErrNoRows { + log.Println("GET [api/locations] " + r.RemoteAddr + ": Location not found (ID " + idStr + ")") + http.Error(w, "Location not found", http.StatusNotFound) + return + } + log.Println("GET [api/locations] DB Error: " + err.Error()) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + json.NewEncoder(w).Encode(loc) + log.Println("GET [api/locations] " + r.RemoteAddr + ": Successfully retrieved location ID " + idStr) + return + } + + rows, err := storage.DB.Query("SELECT id, name FROM locations ORDER BY name ASC") + if err != nil { + log.Println("GET [api/locations] " + r.RemoteAddr + ": " + err.Error()) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + defer rows.Close() + + var locations []models.Location + for rows.Next() { + var loc models.Location + if err := rows.Scan(&loc.ID, &loc.Name); err != nil { + log.Println("GET [api/locations] Scan Error: " + err.Error()) + continue + } + locations = append(locations, loc) + } + + if locations == nil { + locations = []models.Location{} + } + + json.NewEncoder(w).Encode(map[string]interface{}{ + "locations": locations, + }) + log.Println("GET [api/locations] " + r.RemoteAddr + ": Successfully retrieved all locations") + + case http.MethodPost: + var body struct { + Name string `json:"name"` + } + + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + log.Println("POST [api/locations] Decode Error: " + err.Error()) + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + if strings.TrimSpace(body.Name) == "" { + http.Error(w, "Location name cannot be empty", http.StatusBadRequest) + return + } + + res, err := storage.DB.Exec("INSERT INTO locations(name) VALUES(?)", body.Name) + if err != nil { + log.Println("POST [api/locations] DB Error: " + err.Error()) + http.Error(w, "Location already exists or database error", http.StatusConflict) + return + } + + lastID, _ := res.LastInsertId() + + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(models.Location{ + ID: int(lastID), + Name: body.Name, + }) + log.Println("POST [api/locations] " + r.RemoteAddr + ": Successfully created location " + body.Name) + + case http.MethodPut: + idStr := r.URL.Query().Get("id") + if idStr == "" { + http.Error(w, "Missing ID parameter for update", http.StatusBadRequest) + return + } + + id, err := strconv.Atoi(idStr) + if err != nil { + http.Error(w, "Invalid ID parameter", http.StatusBadRequest) + return + } + + var body struct { + Name string `json:"name"` + } + + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + log.Println("PUT [api/locations] Decode Error: " + err.Error()) + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + if strings.TrimSpace(body.Name) == "" { + http.Error(w, "Location name cannot be empty", http.StatusBadRequest) + return + } + + res, err := storage.DB.Exec("UPDATE locations SET name = ? WHERE id = ?", body.Name, id) + if err != nil { + log.Println("PUT [api/locations] DB Error: " + err.Error()) + http.Error(w, "Location name unique constraint failed or database error", http.StatusConflict) + return + } + + rowsAffected, _ := res.RowsAffected() + if rowsAffected == 0 { + http.Error(w, "Location not found", http.StatusNotFound) + return + } + + json.NewEncoder(w).Encode(models.Location{ + ID: id, + Name: body.Name, + }) + log.Println("PUT [api/locations] " + r.RemoteAddr + ": Successfully updated location ID " + idStr) + + case http.MethodDelete: + idStr := r.URL.Query().Get("id") + if idStr == "" { + http.Error(w, "Missing ID parameter for deletion", http.StatusBadRequest) + return + } + + id, err := strconv.Atoi(idStr) + if err != nil { + http.Error(w, "Invalid ID parameter", http.StatusBadRequest) + return + } + + res, err := storage.DB.Exec("DELETE FROM locations WHERE id = ?", id) + if err != nil { + log.Println("DELETE [api/locations] DB Error: " + err.Error()) + http.Error(w, "Cannot delete location. It might still be in use by inventory stock.", http.StatusConflict) + return + } + + rowsAffected, _ := res.RowsAffected() + if rowsAffected == 0 { + http.Error(w, "Location not found", http.StatusNotFound) + return + } + + w.WriteHeader(http.StatusNoContent) + log.Println("DELETE [api/locations] " + r.RemoteAddr + ": Successfully deleted location ID " + idStr) + + default: + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + } +} +func Item(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + switch r.Method { + case http.MethodGet: + idStr := r.URL.Query().Get("id") + + // Read One + if idStr != "" { + id, err := strconv.Atoi(idStr) + if err != nil { + http.Error(w, "Invalid ID parameter", http.StatusBadRequest) + return + } + + var item models.Item + err = storage.DB.QueryRow("SELECT id, name, category, description, total_quantity FROM items WHERE id = ?", id). + Scan(&item.ID, &item.Name, &item.Category, &item.Description, &item.TotalQuantity) + if err != nil { + if err == sql.ErrNoRows { + http.Error(w, "Item not found", http.StatusNotFound) + return + } + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + json.NewEncoder(w).Encode(item) + return + } + + // Read All + rows, err := storage.DB.Query("SELECT id, name, category, description, total_quantity FROM items ORDER BY name ASC") + if err != nil { + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + defer rows.Close() + + items := []models.Item{} + for rows.Next() { + var item models.Item + rows.Scan(&item.ID, &item.Name, &item.Category, &item.Description, &item.TotalQuantity) + items = append(items, item) + } + json.NewEncoder(w).Encode(map[string]interface{}{"items": items}) + + case http.MethodPost: + var body models.Item + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + res, err := storage.DB.Exec("INSERT INTO items(name, category, description, total_quantity) VALUES(?, ?, ?, ?)", + body.Name, body.Category, body.Description, body.TotalQuantity) + if err != nil { + http.Error(w, "Database error", http.StatusInternalServerError) + return + } + + lastID, _ := res.LastInsertId() + body.ID = int(lastID) + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(body) + + case http.MethodPut: + idStr := r.URL.Query().Get("id") + if idStr == "" { + http.Error(w, "Missing ID parameter", http.StatusBadRequest) + return + } + id, _ := strconv.Atoi(idStr) + + var body models.Item + json.NewDecoder(r.Body).Decode(&body) + + res, err := storage.DB.Exec("UPDATE items SET name = ?, category = ?, description = ?, total_quantity = ? WHERE id = ?", + body.Name, body.Category, body.Description, body.TotalQuantity, id) + if err != nil { + http.Error(w, "Database error", http.StatusInternalServerError) + return + } + + rowsAffected, _ := res.RowsAffected() + if rowsAffected == 0 { + http.Error(w, "Item not found", http.StatusNotFound) + return + } + + body.ID = id + json.NewEncoder(w).Encode(body) + + case http.MethodDelete: + idStr := r.URL.Query().Get("id") + id, _ := strconv.Atoi(idStr) + + // SQLite blockiert dank FK, falls Item noch im Stock oder Projekten ist + res, err := storage.DB.Exec("DELETE FROM items WHERE id = ?", id) + if err != nil { + http.Error(w, "Cannot delete item. Still referenced in stock or projects.", http.StatusConflict) + return + } + + rowsAffected, _ := res.RowsAffected() + if rowsAffected == 0 { + http.Error(w, "Item not found", http.StatusNotFound) + return + } + w.WriteHeader(http.StatusNoContent) + + default: + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + } +} +func Project(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + switch r.Method { + case http.MethodGet: + idStr := r.URL.Query().Get("id") + + if idStr != "" { + id, _ := strconv.Atoi(idStr) + var p models.Project + err := storage.DB.QueryRow("SELECT id, name, description FROM projects WHERE id = ?", id).Scan(&p.ID, &p.Name, &p.Description) + if err == sql.ErrNoRows { + http.Error(w, "Project not found", http.StatusNotFound) + return + } + json.NewEncoder(w).Encode(p) + return + } + + rows, err := storage.DB.Query("SELECT id, name, description FROM projects ORDER BY name ASC") + if err != nil { + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + defer rows.Close() + + projects := []models.Project{} + for rows.Next() { + var p models.Project + rows.Scan(&p.ID, &p.Name, &p.Description) + projects = append(projects, p) + } + json.NewEncoder(w).Encode(map[string]interface{}{"projects": projects}) + + case http.MethodPost: + var body models.Project + json.NewDecoder(r.Body).Decode(&body) + + res, err := storage.DB.Exec("INSERT INTO projects(name, description) VALUES(?, ?)", body.Name, body.Description) + if err != nil { + http.Error(w, "Project name already exists", http.StatusConflict) + return + } + + lastID, _ := res.LastInsertId() + body.ID = int(lastID) + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(body) + + case http.MethodPut: + idStr := r.URL.Query().Get("id") + id, _ := strconv.Atoi(idStr) + + var body models.Project + json.NewDecoder(r.Body).Decode(&body) + + res, err := storage.DB.Exec("UPDATE projects SET name = ?, description = ? WHERE id = ?", body.Name, body.Description, id) + if err != nil { + http.Error(w, "Project name constraint violation", http.StatusConflict) + return + } + + rowsAffected, _ := res.RowsAffected() + if rowsAffected == 0 { + http.Error(w, "Project not found", http.StatusNotFound) + return + } + body.ID = id + json.NewEncoder(w).Encode(body) + + case http.MethodDelete: + idStr := r.URL.Query().Get("id") + id, _ := strconv.Atoi(idStr) + + res, err := storage.DB.Exec("DELETE FROM projects WHERE id = ?", id) + if err != nil { + http.Error(w, "Cannot delete project. Clear active item assignments first.", http.StatusConflict) + return + } + + rowsAffected, _ := res.RowsAffected() + if rowsAffected == 0 { + http.Error(w, "Project not found", http.StatusNotFound) + return + } + w.WriteHeader(http.StatusNoContent) + + default: + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + } +} +func Stock(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + switch r.Method { + case http.MethodGet: + idStr := r.URL.Query().Get("id") + + if idStr != "" { + id, _ := strconv.Atoi(idStr) + var s models.Stock + err := storage.DB.QueryRow("SELECT id, item_id, location_id, quantity FROM stock WHERE id = ?", id). + Scan(&s.ID, &s.ItemID, &s.LocationID, &s.Quantity) + if err == sql.ErrNoRows { + http.Error(w, "Stock entry not found", http.StatusNotFound) + return + } + json.NewEncoder(w).Encode(s) + return + } + + rows, err := storage.DB.Query("SELECT id, item_id, location_id, quantity FROM stock") + if err != nil { + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + defer rows.Close() + + stocks := []models.Stock{} + for rows.Next() { + var s models.Stock + rows.Scan(&s.ID, &s.ItemID, &s.LocationID, &s.Quantity) + stocks = append(stocks, s) + } + json.NewEncoder(w).Encode(map[string]interface{}{"stock": stocks}) + + case http.MethodPost: + var body models.Stock + json.NewDecoder(r.Body).Decode(&body) + + res, err := storage.DB.Exec("INSERT INTO stock(item_id, location_id, quantity) VALUES(?, ?, ?)", + body.ItemID, body.LocationID, body.Quantity) + if err != nil { + http.Error(w, "Failed to link stock. Ensure Item and Location exist.", http.StatusBadRequest) + return + } + + lastID, _ := res.LastInsertId() + body.ID = int(lastID) + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(body) + + case http.MethodPut: + idStr := r.URL.Query().Get("id") + id, _ := strconv.Atoi(idStr) + + var body models.Stock + json.NewDecoder(r.Body).Decode(&body) + + res, err := storage.DB.Exec("UPDATE stock SET item_id = ?, location_id = ?, quantity = ? WHERE id = ?", + body.ItemID, body.LocationID, body.Quantity, id) + if err != nil { + http.Error(w, "Failed to update stock. Verify Foreign Keys.", http.StatusBadRequest) + return + } + + rowsAffected, _ := res.RowsAffected() + if rowsAffected == 0 { + http.Error(w, "Stock entry not found", http.StatusNotFound) + return + } + body.ID = id + json.NewEncoder(w).Encode(body) + + case http.MethodDelete: + idStr := r.URL.Query().Get("id") + id, _ := strconv.Atoi(idStr) + + res, _ := storage.DB.Exec("DELETE FROM stock WHERE id = ?", id) + rowsAffected, _ := res.RowsAffected() + if rowsAffected == 0 { + http.Error(w, "Stock entry not found", http.StatusNotFound) + return + } + w.WriteHeader(http.StatusNoContent) + + default: + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + } +} +func Associations(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + switch r.Method { + case http.MethodGet: + idStr := r.URL.Query().Get("id") + projectIDStr := r.URL.Query().Get("project_id") + + // Optionaler Filter: Alle Items für ein bestimmtes Projekt holen (?project_id=X) + if projectIDStr != "" { + pID, _ := strconv.Atoi(projectIDStr) + rows, err := storage.DB.Query("SELECT id, item_id, project_id, quantity FROM project_items WHERE project_id = ?", pID) + if err != nil { + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + defer rows.Close() + + list := []models.ProjectItem{} + for rows.Next() { + var pi models.ProjectItem + rows.Scan(&pi.ID, &pi.ItemID, &pi.ProjectID, &pi.Quantity) + list = append(list, pi) + } + json.NewEncoder(w).Encode(map[string]interface{}{"associations": list}) + return + } + + // Einzelne Assoziation anhand der Tabellen-ID (?id=X) + if idStr != "" { + id, _ := strconv.Atoi(idStr) + var pi models.ProjectItem + err := storage.DB.QueryRow("SELECT id, item_id, project_id, quantity FROM project_items WHERE id = ?", id). + Scan(&pi.ID, &pi.ItemID, &pi.ProjectID, &pi.Quantity) + if err == sql.ErrNoRows { + http.Error(w, "Association not found", http.StatusNotFound) + return + } + json.NewEncoder(w).Encode(pi) + return + } + + // Gar kein Parameter -> Komplett-Dump aller Zuweisungen + rows, err := storage.DB.Query("SELECT id, item_id, project_id, quantity FROM project_items") + if err != nil { + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + defer rows.Close() + + all := []models.ProjectItem{} + for rows.Next() { + var pi models.ProjectItem + rows.Scan(&pi.ID, &pi.ItemID, &pi.ProjectID, &pi.Quantity) + all = append(all, pi) + } + json.NewEncoder(w).Encode(map[string]interface{}{"associations": all}) + + case http.MethodPost: + var body models.ProjectItem + json.NewDecoder(r.Body).Decode(&body) + + res, err := storage.DB.Exec("INSERT INTO project_items(item_id, project_id, quantity) VALUES(?, ?, ?)", + body.ItemID, body.ProjectID, body.Quantity) + if err != nil { + http.Error(w, "Link failed. Check item_id and project_id.", http.StatusBadRequest) + return + } + + lastID, _ := res.LastInsertId() + body.ID = int(lastID) + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(body) + + case http.MethodPut: + idStr := r.URL.Query().Get("id") + id, _ := strconv.Atoi(idStr) + + var body models.ProjectItem + json.NewDecoder(r.Body).Decode(&body) + + res, err := storage.DB.Exec("UPDATE project_items SET item_id = ?, project_id = ?, quantity = ? WHERE id = ?", + body.ItemID, body.ProjectID, body.Quantity, id) + if err != nil { + http.Error(w, "Update failed. Check Constraints.", http.StatusBadRequest) + return + } + + rowsAffected, _ := res.RowsAffected() + if rowsAffected == 0 { + http.Error(w, "Association not found", http.StatusNotFound) + return + } + body.ID = id + json.NewEncoder(w).Encode(body) + + case http.MethodDelete: + idStr := r.URL.Query().Get("id") + id, _ := strconv.Atoi(idStr) + + res, _ := storage.DB.Exec("DELETE FROM project_items WHERE id = ?", id) + rowsAffected, _ := res.RowsAffected() + if rowsAffected == 0 { + http.Error(w, "Association not found", http.StatusNotFound) + return + } + w.WriteHeader(http.StatusNoContent) + + default: + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + } +} diff --git a/handlers/items.go b/handlers/items.go deleted file mode 100644 index 089c651..0000000 --- a/handlers/items.go +++ /dev/null @@ -1,41 +0,0 @@ -package handlers - -import ( - "MiauInv/models" - "MiauInv/storage" - "encoding/json" - "net/http" -) - -func GetItems(w http.ResponseWriter, r *http.Request) { - - items, err := storage.GetItems() - - if err != nil { - http.Error(w, err.Error(), 500) - return - } - - json.NewEncoder(w).Encode(items) -} - -func CreateItem(w http.ResponseWriter, r *http.Request) { - - var item models.Item - - err := json.NewDecoder(r.Body).Decode(&item) - - if err != nil { - http.Error(w, err.Error(), 400) - return - } - - err = storage.AddItem(item) - - if err != nil { - http.Error(w, err.Error(), 500) - return - } - - w.WriteHeader(http.StatusCreated) -} diff --git a/handlers/locations.go b/handlers/locations.go deleted file mode 100644 index 8fa235e..0000000 --- a/handlers/locations.go +++ /dev/null @@ -1,20 +0,0 @@ -package handlers - -import ( - "MiauInv/storage" - "net/http" -) - -func CreateLocation(w http.ResponseWriter, r *http.Request) { - name := r.FormValue("name") - - _, err := storage.DB.Exec( - "INSERT INTO locations(name) VALUES(?)", - name, - ) - - if err != nil { - http.Error(w, err.Error(), 500) - return - } -} diff --git a/handlers/project_items.go b/handlers/project_items.go deleted file mode 100644 index 9cf41aa..0000000 --- a/handlers/project_items.go +++ /dev/null @@ -1,34 +0,0 @@ -package handlers - -import ( - "MiauInv/storage" - "encoding/json" - "net/http" -) - -type ProjectItemRequest struct { - ItemID int `json:"item_id"` - ProjectID int `json:"project_id"` - Quantity int `json:"quantity"` -} - -func AllocateToProject(w http.ResponseWriter, r *http.Request) { - - var req ProjectItemRequest - - json.NewDecoder(r.Body).Decode(&req) - - _, err := storage.DB.Exec(` - INSERT INTO project_items(item_id,project_id,quantity) - VALUES(?,?,?) - `, - req.ItemID, - req.ProjectID, - req.Quantity, - ) - - if err != nil { - http.Error(w, err.Error(), 500) - return - } -} diff --git a/handlers/projects.go b/handlers/projects.go deleted file mode 100644 index 9d52602..0000000 --- a/handlers/projects.go +++ /dev/null @@ -1,21 +0,0 @@ -package handlers - -import ( - "MiauInv/storage" - "net/http" -) - -func CreateProject(w http.ResponseWriter, r *http.Request) { - - name := r.FormValue("name") - - _, err := storage.DB.Exec( - "INSERT INTO projects(name) VALUES(?)", - name, - ) - - if err != nil { - http.Error(w, err.Error(), 500) - return - } -} diff --git a/handlers/stock.go b/handlers/stock.go deleted file mode 100644 index 4826dbb..0000000 --- a/handlers/stock.go +++ /dev/null @@ -1,34 +0,0 @@ -package handlers - -import ( - "MiauInv/storage" - "encoding/json" - "net/http" -) - -type StockRequest struct { - ItemID int `json:"item_id"` - LocationID int `json:"location_id"` - Quantity int `json:"quantity"` -} - -func AddStock(w http.ResponseWriter, r *http.Request) { - - var req StockRequest - - json.NewDecoder(r.Body).Decode(&req) - - _, err := storage.DB.Exec(` - INSERT INTO stock(item_id,location_id,quantity) - VALUES(?,?,?) - `, - req.ItemID, - req.LocationID, - req.Quantity, - ) - - if err != nil { - http.Error(w, err.Error(), 500) - return - } -} diff --git a/server/server.go b/server/server.go index c63acfa..2929969 100644 --- a/server/server.go +++ b/server/server.go @@ -77,11 +77,11 @@ func (this *Server) Run() { mux.HandleFunc("/api/refresh", handlers.RefreshToken) mux.Handle("/api/logout", auth.AuthMiddleware(this.JWTSecret)(http.HandlerFunc(handlers.Logout))) - mux.HandleFunc("/api/item", handlers.GetItems) - mux.HandleFunc("/api/locations", handlers.CreateLocation) - mux.HandleFunc("/api/project", handlers.CreateProject) - mux.HandleFunc("/api/stock/add", handlers.AddStock) - mux.HandleFunc("/api/project-items/add", handlers.AllocateToProject) + mux.Handle("/api/item", auth.AuthMiddleware(this.JWTSecret)(http.HandlerFunc(handlers.Item))) + mux.Handle("/api/location", auth.AuthMiddleware(this.JWTSecret)(http.HandlerFunc(handlers.Location))) + mux.Handle("/api/project", auth.AuthMiddleware(this.JWTSecret)(http.HandlerFunc(handlers.Project))) + mux.Handle("/api/stock", auth.AuthMiddleware(this.JWTSecret)(http.HandlerFunc(handlers.Stock))) + mux.Handle("/api/association", auth.AuthMiddleware(this.JWTSecret)(http.HandlerFunc(handlers.Associations))) // Assets mux.HandleFunc("/assets/", frontend.Assets)