package handlers import ( "encoding/json" "net/http" "strconv" "mgit.msbls.de/m/KanzlAI-mGMT/internal/auth" "mgit.msbls.de/m/KanzlAI-mGMT/internal/services" "github.com/google/uuid" ) type TimeEntryHandler struct { svc *services.TimeEntryService } func NewTimeEntryHandler(svc *services.TimeEntryService) *TimeEntryHandler { return &TimeEntryHandler{svc: svc} } // ListForCase handles GET /api/cases/{id}/time-entries func (h *TimeEntryHandler) ListForCase(w http.ResponseWriter, r *http.Request) { tenantID, ok := auth.TenantFromContext(r.Context()) if !ok { writeError(w, http.StatusForbidden, "missing tenant") return } caseID, err := parsePathUUID(r, "id") if err != nil { writeError(w, http.StatusBadRequest, "invalid case ID") return } entries, err := h.svc.ListForCase(r.Context(), tenantID, caseID) if err != nil { internalError(w, "failed to list time entries", err) return } writeJSON(w, http.StatusOK, map[string]any{"time_entries": entries}) } // List handles GET /api/time-entries?case_id=&user_id=&from=&to= func (h *TimeEntryHandler) List(w http.ResponseWriter, r *http.Request) { tenantID, ok := auth.TenantFromContext(r.Context()) if !ok { writeError(w, http.StatusForbidden, "missing tenant") return } limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) offset, _ := strconv.Atoi(r.URL.Query().Get("offset")) limit, offset = clampPagination(limit, offset) filter := services.TimeEntryFilter{ From: r.URL.Query().Get("from"), To: r.URL.Query().Get("to"), Limit: limit, Offset: offset, } if caseStr := r.URL.Query().Get("case_id"); caseStr != "" { caseID, err := uuid.Parse(caseStr) if err != nil { writeError(w, http.StatusBadRequest, "invalid case_id") return } filter.CaseID = &caseID } if userStr := r.URL.Query().Get("user_id"); userStr != "" { userID, err := uuid.Parse(userStr) if err != nil { writeError(w, http.StatusBadRequest, "invalid user_id") return } filter.UserID = &userID } entries, total, err := h.svc.List(r.Context(), tenantID, filter) if err != nil { internalError(w, "failed to list time entries", err) return } writeJSON(w, http.StatusOK, map[string]any{ "time_entries": entries, "total": total, }) } // Create handles POST /api/cases/{id}/time-entries func (h *TimeEntryHandler) Create(w http.ResponseWriter, r *http.Request) { tenantID, ok := auth.TenantFromContext(r.Context()) if !ok { writeError(w, http.StatusForbidden, "missing tenant") return } userID, _ := auth.UserFromContext(r.Context()) caseID, err := parsePathUUID(r, "id") if err != nil { writeError(w, http.StatusBadRequest, "invalid case ID") return } var input services.CreateTimeEntryInput if err := json.NewDecoder(r.Body).Decode(&input); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON body") return } input.CaseID = caseID if input.Description == "" { writeError(w, http.StatusBadRequest, "description is required") return } if input.DurationMinutes <= 0 { writeError(w, http.StatusBadRequest, "duration_minutes must be positive") return } if input.Date == "" { writeError(w, http.StatusBadRequest, "date is required") return } entry, err := h.svc.Create(r.Context(), tenantID, userID, input) if err != nil { internalError(w, "failed to create time entry", err) return } writeJSON(w, http.StatusCreated, entry) } // Update handles PUT /api/time-entries/{id} func (h *TimeEntryHandler) Update(w http.ResponseWriter, r *http.Request) { tenantID, ok := auth.TenantFromContext(r.Context()) if !ok { writeError(w, http.StatusForbidden, "missing tenant") return } entryID, err := parsePathUUID(r, "id") if err != nil { writeError(w, http.StatusBadRequest, "invalid time entry ID") return } var input services.UpdateTimeEntryInput if err := json.NewDecoder(r.Body).Decode(&input); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON body") return } entry, err := h.svc.Update(r.Context(), tenantID, entryID, input) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusOK, entry) } // Delete handles DELETE /api/time-entries/{id} func (h *TimeEntryHandler) Delete(w http.ResponseWriter, r *http.Request) { tenantID, ok := auth.TenantFromContext(r.Context()) if !ok { writeError(w, http.StatusForbidden, "missing tenant") return } entryID, err := parsePathUUID(r, "id") if err != nil { writeError(w, http.StatusBadRequest, "invalid time entry ID") return } if err := h.svc.Delete(r.Context(), tenantID, entryID); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"}) } // Summary handles GET /api/time-entries/summary?group_by=case|user|month&from=&to= func (h *TimeEntryHandler) Summary(w http.ResponseWriter, r *http.Request) { tenantID, ok := auth.TenantFromContext(r.Context()) if !ok { writeError(w, http.StatusForbidden, "missing tenant") return } groupBy := r.URL.Query().Get("group_by") if groupBy == "" { groupBy = "case" } summaries, err := h.svc.Summary(r.Context(), tenantID, groupBy, r.URL.Query().Get("from"), r.URL.Query().Get("to")) if err != nil { internalError(w, "failed to get summary", err) return } writeJSON(w, http.StatusOK, map[string]any{"summary": summaries}) }