Files
KanzlAI-mGMT/backend/internal/handlers/appointments.go
m 9bd8cc9e07 feat: add document upload/download backend (Phase 2K)
- StorageClient for Supabase Storage REST API (upload, download, delete)
- DocumentService with CRUD operations + storage integration
- DocumentHandler with multipart form upload support (50MB limit)
- Routes: GET/POST /api/cases/{id}/documents, GET/DELETE /api/documents/{docId}
- file_path format: {tenant_id}/{case_id}/{uuid}_{filename}
- Case events logged on upload/delete
- Added SUPABASE_SERVICE_KEY to config for server-side storage access
- Fixed pre-existing duplicate writeJSON/writeError in appointments.go
2026-03-25 13:40:19 +01:00

206 lines
5.3 KiB
Go

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}
}
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 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 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)
}