package commandcenter import ( "encoding/json" "errors" "net/http" "time" "github.com/go-chi/chi/v5" "github.com/missionworkforce/missionmatch/internal/platform/security" "github.com/missionworkforce/missionmatch/internal/platform/tenant" ) // Handler serves the Workforce Command Center endpoint. type Handler struct{ svc *Service } func NewHandler(svc *Service) *Handler { return &Handler{svc: svc} } // Routes mounts the Command Center endpoints under the tenant scoped group. The dashboard read is gated // on candidate.read since the payload exposes candidate names and clearance (the same data class as GET // /candidates). Adopting a forecast as a requirement is a write, gated on requirement.write. func (h *Handler) Routes(r chi.Router) { r.With(security.RequireCapability(security.CapCandidateRead)).Get("/command-center", h.get) // The federal signal panel reads public federal data and the global mission tag and incumbent role // catalogs. It is gated on candidate.read like the dashboard since it shares the same surface, and it // is read only. The optional mission query parameter scopes the open by mission list to one code. r.With(security.RequireCapability(security.CapCandidateRead)).Get("/command-center/federal-signal", h.federalSignal) r.With(security.RequireCapability(security.CapRequirementEdit)).Post("/command-center/upcoming/{recordID}/adopt", h.adopt) } func (h *Handler) federalSignal(w http.ResponseWriter, r *http.Request) { mission := r.URL.Query().Get("mission") v, err := h.svc.FederalSignal(r.Context(), mission) if err != nil { switch { case errors.Is(err, errUnknownMission): writeJSONErr(w, http.StatusBadRequest, "unknown mission code") case errors.Is(err, tenant.ErrNoTenant): writeJSONErr(w, http.StatusForbidden, "no validated tenant") case errors.Is(err, ErrNoDatabase): writeJSONErr(w, http.StatusServiceUnavailable, "database not configured") default: writeJSONErr(w, http.StatusInternalServerError, "internal error") } return } w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(v) } func (h *Handler) adopt(w http.ResponseWriter, r *http.Request) { recordID := chi.URLParam(r, "recordID") var in AdoptForecastInput dec := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)) dec.DisallowUnknownFields() if err := dec.Decode(&in); err != nil { writeJSONErr(w, http.StatusBadRequest, "invalid request body") return } res, err := h.svc.AdoptForecast(r.Context(), recordID, in) if err != nil { switch { case errors.Is(err, ErrForecastNotFound): writeJSONErr(w, http.StatusNotFound, "forecast not found") case errors.Is(err, ErrNotAForecast), errors.Is(err, ErrBadRequest): writeJSONErr(w, http.StatusBadRequest, "record is not an adoptable forecast") case errors.Is(err, tenant.ErrNoTenant): writeJSONErr(w, http.StatusForbidden, "no validated tenant") case errors.Is(err, ErrNoDatabase): writeJSONErr(w, http.StatusServiceUnavailable, "database not configured") default: writeJSONErr(w, http.StatusInternalServerError, "internal error") } return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) _ = json.NewEncoder(w).Encode(res) } func writeJSONErr(w http.ResponseWriter, status int, msg string) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(map[string]string{"error": msg}) } func (h *Handler) get(w http.ResponseWriter, r *http.Request) { asOf := h.svc.clock() if q := r.URL.Query().Get("as_of"); q != "" { if t, err := time.Parse("2006-01-02", q); err == nil { asOf = t } } v, err := h.svc.CommandCenter(r.Context(), asOf) if err != nil { w.Header().Set("Content-Type", "application/json") switch { case errors.Is(err, tenant.ErrNoTenant): w.WriteHeader(http.StatusForbidden) _, _ = w.Write([]byte(`{"error":"no validated tenant"}`)) case errors.Is(err, ErrNoDatabase): w.WriteHeader(http.StatusServiceUnavailable) _, _ = w.Write([]byte(`{"error":"database not configured"}`)) default: w.WriteHeader(http.StatusInternalServerError) _, _ = w.Write([]byte(`{"error":"internal error"}`)) } return } w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(v) }