package server import ( "net/http" "time" "task/internal/sqlite" "task/internal/ticket" ) func (s *Server) handleListProjectTime(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") if _, ok, code := s.requireProjectRole(id, sqlite.RoleViewer, r); !ok { writeError(w, code, "forbidden") return } list, err := s.Times.ListByProject(id) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, list) } func (s *Server) handleCreateTime(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") u, ok, code := s.requireProjectRole(id, sqlite.RoleMember, r) if !ok { writeError(w, code, "forbidden") return } var body struct { TicketID string `json:"ticketId"` Duration int64 `json:"durationNs"` Note string `json:"note"` Source string `json:"source"` } if err := readJSON(r, &body); err != nil { writeError(w, http.StatusBadRequest, "invalid json") return } e, err := s.Times.Create(ticket.TimeEntry{ TicketID: body.TicketID, ProjectID: id, Duration: time.Duration(body.Duration), Note: body.Note, Source: body.Source, }) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } _, _ = s.DB.AppendRevision("time_entry", e.ID, id, "create", u.ID, e) writeJSON(w, http.StatusCreated, e) } func (s *Server) handleUpdateTime(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") eid := r.PathValue("eid") u, ok, code := s.requireProjectRole(id, sqlite.RoleMember, r) if !ok { writeError(w, code, "forbidden") return } var body struct { Duration int64 `json:"durationNs"` Note string `json:"note"` } if err := readJSON(r, &body); err != nil { writeError(w, http.StatusBadRequest, "invalid json") return } entries, err := s.Times.ListByProject(id) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } var found *ticket.TimeEntry for i := range entries { if entries[i].ID == eid { found = &entries[i] break } } if found == nil { writeError(w, http.StatusNotFound, "not found") return } if body.Duration > 0 { found.Duration = time.Duration(body.Duration) } found.Note = body.Note if err := s.Times.Update(*found); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } _, _ = s.DB.AppendRevision("time_entry", found.ID, id, "update", u.ID, found) writeJSON(w, http.StatusOK, found) } func (s *Server) handleDeleteTime(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") eid := r.PathValue("eid") u, ok, code := s.requireProjectRole(id, sqlite.RoleMember, r) if !ok { writeError(w, code, "forbidden") return } entries, _ := s.Times.ListByProject(id) for _, e := range entries { if e.ID == eid { _, _ = s.DB.AppendRevision("time_entry", e.ID, id, "delete", u.ID, e) break } } if err := s.Times.Delete(eid); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) } func (s *Server) handleGetTimer(w http.ResponseWriter, r *http.Request) { t, err := s.Times.LoadActiveTimer() if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } if t == nil { writeJSON(w, http.StatusOK, map[string]any{"timer": nil}) return } writeJSON(w, http.StatusOK, map[string]any{"timer": t}) } func (s *Server) handleStartTimer(w http.ResponseWriter, r *http.Request) { u, _ := s.currentUser(r) var body struct { TicketID string `json:"ticketId"` ProjectID string `json:"projectId"` } if err := readJSON(r, &body); err != nil { writeError(w, http.StatusBadRequest, "invalid json") return } if _, ok, code := s.requireProjectRole(body.ProjectID, sqlite.RoleMember, r); !ok { writeError(w, code, "forbidden") return } t := &ticket.ActiveTimer{ TicketID: body.TicketID, ProjectID: body.ProjectID, StartedAt: time.Now().UTC(), } if err := s.Times.SaveActiveTimer(t); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } _ = u writeJSON(w, http.StatusOK, t) } func (s *Server) handleStopTimer(w http.ResponseWriter, r *http.Request) { u, _ := s.currentUser(r) active, err := s.Times.LoadActiveTimer() if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } if active == nil { writeError(w, http.StatusBadRequest, "no active timer") return } if _, ok, code := s.requireProjectRole(active.ProjectID, sqlite.RoleMember, r); !ok { writeError(w, code, "forbidden") return } dur := time.Since(active.StartedAt) if dur < time.Second { dur = time.Second } e, err := s.Times.Create(ticket.TimeEntry{ TicketID: active.TicketID, ProjectID: active.ProjectID, Start: active.StartedAt, End: time.Now().UTC(), Duration: dur, Source: ticket.SourceTimer, }) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } _ = s.Times.SaveActiveTimer(nil) _, _ = s.DB.AppendRevision("time_entry", e.ID, active.ProjectID, "create", u.ID, e) writeJSON(w, http.StatusOK, e) }