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") contentMode := r.URL.Query().Get("content") if idStr != "" && contentMode == "true" { locationID, err := strconv.Atoi(idStr) if err != nil { http.Error(w, "Invalid location ID", http.StatusBadRequest) return } query := ` SELECT s.item_id, i.name, s.quantity FROM stock s JOIN items i ON s.item_id = i.id WHERE s.location_id = ? AND s.quantity > 0 ` rows, err := storage.DB.Query(query, locationID) if err != nil { http.Error(w, "Database error", http.StatusInternalServerError) return } defer rows.Close() var contents []models.LocationContent for rows.Next() { var c models.LocationContent if err := rows.Scan(&c.ItemID, &c.ItemName, &c.Quantity); err != nil { http.Error(w, "Row scan error", http.StatusInternalServerError) return } contents = append(contents, c) } json.NewEncoder(w).Encode(map[string]interface{}{"contents": contents}) return } 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") 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 { 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/item] DB Error: " + err.Error()) http.Error(w, "Internal server error", http.StatusInternalServerError) return } json.NewEncoder(w).Encode(item) log.Println("GET [api/item] " + r.RemoteAddr + ": Successfully retrieved item ID " + idStr) return } query := ` SELECT i.id, i.name, i.category, i.description, COALESCE((SELECT SUM(quantity) FROM stock WHERE item_id = i.id), 0) as total_qty, COALESCE((SELECT SUM(quantity) FROM project_items WHERE item_id = i.id), 0) as allocated_qty FROM items i ORDER BY i.name ASC ` rows, err := storage.DB.Query(query) if err != nil { http.Error(w, "Database error", http.StatusInternalServerError) return } defer rows.Close() var itemsExtended []models.ItemExtended for rows.Next() { var ie models.ItemExtended err := rows.Scan(&ie.ID, &ie.Name, &ie.Category, &ie.Description, &ie.TotalQuantity, &ie.AllocatedQty) if err != nil { continue } ie.AvailableQty = ie.TotalQuantity - ie.AllocatedQty itemsExtended = append(itemsExtended, ie) } json.NewEncoder(w).Encode(map[string]interface{}{"items": itemsExtended}) return 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") detailsMode := r.URL.Query().Get("details") if idStr != "" && detailsMode == "true" { projectID, err := strconv.Atoi(idStr) if err != nil { http.Error(w, "Invalid project ID", http.StatusBadRequest) return } query := ` SELECT pi.item_id, i.name, pi.quantity FROM project_items pi JOIN items i ON pi.item_id = i.id WHERE pi.project_id = ? ` rows, err := storage.DB.Query(query, projectID) if err != nil { http.Error(w, "Database error", http.StatusInternalServerError) return } defer rows.Close() var details []models.ProjectDetailItem for rows.Next() { var d models.ProjectDetailItem if err := rows.Scan(&d.ItemID, &d.ItemName, &d.Quantity); err != nil { http.Error(w, "Row scan error", http.StatusInternalServerError) return } details = append(details, d) } json.NewEncoder(w).Encode(map[string]interface{}{"items": details}) return } 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") 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 } 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 } 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) } }