package handler import ( "errors" "net/http" "github.com/go-chi/chi/v5" "github.com/wotra/wotra/internal/service" ) // EntryHandler serves /api/entries routes. type EntryHandler struct { svc *service.EntryService } func NewEntryHandler(svc *service.EntryService) *EntryHandler { return &EntryHandler{svc: svc} } func (h *EntryHandler) Routes(r chi.Router) { r.Post("/entries/start", h.Start) r.Post("/entries", h.CreateInterval) r.Post("/entries/{id}/stop", h.StopByID) r.Get("/entries", h.List) r.Put("/entries/{id}", h.Update) r.Delete("/entries/{id}", h.Delete) } // CreateInterval POST /api/entries — create a completed interval with explicit times func (h *EntryHandler) CreateInterval(w http.ResponseWriter, r *http.Request) { var body struct { StartTime int64 `json:"start_time"` EndTime int64 `json:"end_time"` Note string `json:"note"` } if err := decodeJSON(r, &body); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON") return } if body.StartTime == 0 || body.EndTime == 0 { writeError(w, http.StatusBadRequest, "start_time and end_time are required") return } entry, err := h.svc.CreateInterval(r.Context(), service.CreateIntervalInput{ StartTime: body.StartTime, EndTime: body.EndTime, Note: body.Note, }) if err != nil { switch { case errors.Is(err, service.ErrCrossesMidnight): writeError(w, http.StatusUnprocessableEntity, err.Error()) case errors.Is(err, service.ErrDayAlreadyClosed): writeError(w, http.StatusConflict, err.Error()) default: writeError(w, http.StatusInternalServerError, err.Error()) } return } writeJSON(w, http.StatusCreated, entry) } // Start POST /api/entries/start func (h *EntryHandler) Start(w http.ResponseWriter, r *http.Request) { var body struct { Note string `json:"note"` } _ = decodeJSON(r, &body) // note is optional entry, err := h.svc.Start(r.Context(), body.Note) if err != nil { switch { case errors.Is(err, service.ErrEntryRunning): writeError(w, http.StatusConflict, err.Error()) case errors.Is(err, service.ErrDayAlreadyClosed): writeError(w, http.StatusConflict, err.Error()) default: writeError(w, http.StatusInternalServerError, err.Error()) } return } writeJSON(w, http.StatusCreated, entry) } // StopByID POST /api/entries/{id}/stop func (h *EntryHandler) StopByID(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") entry, err := h.svc.StopByID(r.Context(), id) if err != nil { switch { case errors.Is(err, service.ErrEntryNotFound): writeError(w, http.StatusNotFound, err.Error()) case errors.Is(err, service.ErrEntryNotRunning): writeError(w, http.StatusConflict, err.Error()) default: writeError(w, http.StatusInternalServerError, err.Error()) } return } writeJSON(w, http.StatusOK, entry) } // List GET /api/entries?from=YYYY-MM-DD&to=YYYY-MM-DD func (h *EntryHandler) List(w http.ResponseWriter, r *http.Request) { from := r.URL.Query().Get("from") to := r.URL.Query().Get("to") if from == "" { from = "0000-01-01" } if to == "" { to = "9999-12-31" } entries, err := h.svc.List(r.Context(), from, to) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } if entries == nil { writeJSON(w, http.StatusOK, []struct{}{}) return } writeJSON(w, http.StatusOK, entries) } // Update PUT /api/entries/{id} func (h *EntryHandler) Update(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") var body struct { StartTime *int64 `json:"start_time"` EndTime *int64 `json:"end_time"` Note *string `json:"note"` } if err := decodeJSON(r, &body); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON") return } entry, err := h.svc.Update(r.Context(), id, service.UpdateEntryInput{ StartTime: body.StartTime, EndTime: body.EndTime, Note: body.Note, }) if err != nil { switch { case errors.Is(err, service.ErrEntryNotFound): writeError(w, http.StatusNotFound, err.Error()) case errors.Is(err, service.ErrCrossesMidnight): writeError(w, http.StatusUnprocessableEntity, err.Error()) default: writeError(w, http.StatusInternalServerError, err.Error()) } return } writeJSON(w, http.StatusOK, entry) } // Delete DELETE /api/entries/{id} func (h *EntryHandler) Delete(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") if err := h.svc.Delete(r.Context(), id); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } w.WriteHeader(http.StatusNoContent) }