package handlers import ( "database/sql" "encoding/json" "errors" "net/http" "time" "github.com/google/uuid" "mgit.msbls.de/m/KanzlAI-mGMT/internal/auth" "mgit.msbls.de/m/KanzlAI-mGMT/internal/models" "mgit.msbls.de/m/KanzlAI-mGMT/internal/services" ) type AppointmentHandler struct { svc *services.AppointmentService } func NewAppointmentHandler(svc *services.AppointmentService) *AppointmentHandler { return &AppointmentHandler{svc: svc} } // Get handles GET /api/appointments/{id} func (h *AppointmentHandler) Get(w http.ResponseWriter, r *http.Request) { tenantID, ok := auth.TenantFromContext(r.Context()) if !ok { writeError(w, http.StatusUnauthorized, "missing tenant") return } id, err := uuid.Parse(r.PathValue("id")) if err != nil { writeError(w, http.StatusBadRequest, "invalid appointment id") return } appt, err := h.svc.GetByID(r.Context(), tenantID, id) if err != nil { if errors.Is(err, sql.ErrNoRows) { writeError(w, http.StatusNotFound, "appointment not found") return } writeError(w, http.StatusInternalServerError, "failed to fetch appointment") return } writeJSON(w, http.StatusOK, appt) } func (h *AppointmentHandler) List(w http.ResponseWriter, r *http.Request) { tenantID, ok := auth.TenantFromContext(r.Context()) if !ok { writeError(w, http.StatusUnauthorized, "missing tenant") return } filter := services.AppointmentFilter{} if v := r.URL.Query().Get("case_id"); v != "" { id, err := uuid.Parse(v) if err != nil { writeError(w, http.StatusBadRequest, "invalid case_id") return } filter.CaseID = &id } if v := r.URL.Query().Get("type"); v != "" { filter.Type = &v } if v := r.URL.Query().Get("start_from"); v != "" { t, err := time.Parse(time.RFC3339, v) if err != nil { writeError(w, http.StatusBadRequest, "invalid start_from (use RFC3339)") return } filter.StartFrom = &t } if v := r.URL.Query().Get("start_to"); v != "" { t, err := time.Parse(time.RFC3339, v) if err != nil { writeError(w, http.StatusBadRequest, "invalid start_to (use RFC3339)") return } filter.StartTo = &t } appointments, err := h.svc.List(r.Context(), tenantID, filter) if err != nil { writeError(w, http.StatusInternalServerError, "failed to list appointments") return } writeJSON(w, http.StatusOK, appointments) } type createAppointmentRequest struct { CaseID *uuid.UUID `json:"case_id"` Title string `json:"title"` Description *string `json:"description"` StartAt time.Time `json:"start_at"` EndAt *time.Time `json:"end_at"` Location *string `json:"location"` AppointmentType *string `json:"appointment_type"` } func (h *AppointmentHandler) Create(w http.ResponseWriter, r *http.Request) { tenantID, ok := auth.TenantFromContext(r.Context()) if !ok { writeError(w, http.StatusUnauthorized, "missing tenant") return } var req createAppointmentRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request body") return } if req.Title == "" { writeError(w, http.StatusBadRequest, "title is required") return } if msg := validateStringLength("title", req.Title, maxTitleLen); msg != "" { writeError(w, http.StatusBadRequest, msg) return } if req.StartAt.IsZero() { writeError(w, http.StatusBadRequest, "start_at is required") return } appt := &models.Appointment{ TenantID: tenantID, CaseID: req.CaseID, Title: req.Title, Description: req.Description, StartAt: req.StartAt, EndAt: req.EndAt, Location: req.Location, AppointmentType: req.AppointmentType, } if err := h.svc.Create(r.Context(), appt); err != nil { writeError(w, http.StatusInternalServerError, "failed to create appointment") return } writeJSON(w, http.StatusCreated, appt) } type updateAppointmentRequest struct { CaseID *uuid.UUID `json:"case_id"` Title string `json:"title"` Description *string `json:"description"` StartAt time.Time `json:"start_at"` EndAt *time.Time `json:"end_at"` Location *string `json:"location"` AppointmentType *string `json:"appointment_type"` } func (h *AppointmentHandler) Update(w http.ResponseWriter, r *http.Request) { tenantID, ok := auth.TenantFromContext(r.Context()) if !ok { writeError(w, http.StatusUnauthorized, "missing tenant") return } id, err := uuid.Parse(r.PathValue("id")) if err != nil { writeError(w, http.StatusBadRequest, "invalid appointment id") return } // Fetch existing to verify ownership existing, err := h.svc.GetByID(r.Context(), tenantID, id) if err != nil { if errors.Is(err, sql.ErrNoRows) { writeError(w, http.StatusNotFound, "appointment not found") return } writeError(w, http.StatusInternalServerError, "failed to fetch appointment") return } var req updateAppointmentRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request body") return } if req.Title == "" { writeError(w, http.StatusBadRequest, "title is required") return } if msg := validateStringLength("title", req.Title, maxTitleLen); msg != "" { writeError(w, http.StatusBadRequest, msg) return } if req.StartAt.IsZero() { writeError(w, http.StatusBadRequest, "start_at is required") return } existing.CaseID = req.CaseID existing.Title = req.Title existing.Description = req.Description existing.StartAt = req.StartAt existing.EndAt = req.EndAt existing.Location = req.Location existing.AppointmentType = req.AppointmentType if err := h.svc.Update(r.Context(), existing); err != nil { writeError(w, http.StatusInternalServerError, "failed to update appointment") return } writeJSON(w, http.StatusOK, existing) } func (h *AppointmentHandler) Delete(w http.ResponseWriter, r *http.Request) { tenantID, ok := auth.TenantFromContext(r.Context()) if !ok { writeError(w, http.StatusUnauthorized, "missing tenant") return } id, err := uuid.Parse(r.PathValue("id")) if err != nil { writeError(w, http.StatusBadRequest, "invalid appointment id") return } if err := h.svc.Delete(r.Context(), tenantID, id); err != nil { writeError(w, http.StatusNotFound, "appointment not found") return } w.WriteHeader(http.StatusNoContent) }