Full event-driven deadline determination system ported from youpc.org:
Backend:
- DetermineService: walks proceeding event tree, calculates cascading
dates with holiday adjustment and conditional logic
- GET /api/proceeding-types/{code}/timeline — full event tree structure
- POST /api/deadlines/determine — calculate timeline with conditions
- POST /api/cases/{caseID}/deadlines/batch — batch-create deadlines
- DeadlineRule model: added is_spawn, spawn_label fields
- GetFullTimeline: recursive CTE following cross-type spawn branches
- Conditional deadlines: condition_rule_id toggles alt_duration/rule_code
(e.g. Reply changes from RoP.029b to RoP.029a when CCR is filed)
- Seed SQL with full UPC event trees (INF, REV, CCR, APM, APP, AMD)
Frontend:
- DeadlineWizard: interactive proceeding timeline with step-by-step flow
1. Select proceeding type (visual cards)
2. Enter trigger event date
3. Toggle conditional branches (CCR, Appeal, Amend)
4. See full calculated timeline with color-coded urgency
5. Batch-create all deadlines on a selected case
- Visual timeline tree with party icons, rule codes, duration badges
- Kept existing DeadlineCalculator as "Schnell" quick mode
Also resolved merge conflicts across 6 files (auth, router, handlers)
merging role-based permissions + audit trail features.
128 lines
3.5 KiB
Go
128 lines
3.5 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"mgit.msbls.de/m/KanzlAI-mGMT/internal/auth"
|
|
"mgit.msbls.de/m/KanzlAI-mGMT/internal/services"
|
|
)
|
|
|
|
// DetermineHandlers holds handlers for deadline determination endpoints
|
|
type DetermineHandlers struct {
|
|
determine *services.DetermineService
|
|
deadlines *services.DeadlineService
|
|
}
|
|
|
|
// NewDetermineHandlers creates determine handlers
|
|
func NewDetermineHandlers(determine *services.DetermineService, deadlines *services.DeadlineService) *DetermineHandlers {
|
|
return &DetermineHandlers{determine: determine, deadlines: deadlines}
|
|
}
|
|
|
|
// GetTimeline handles GET /api/proceeding-types/{code}/timeline
|
|
// Returns the full event tree for a proceeding type (no date calculations)
|
|
func (h *DetermineHandlers) GetTimeline(w http.ResponseWriter, r *http.Request) {
|
|
code := r.PathValue("code")
|
|
if code == "" {
|
|
writeError(w, http.StatusBadRequest, "proceeding type code required")
|
|
return
|
|
}
|
|
|
|
timeline, pt, err := h.determine.GetTimeline(code)
|
|
if err != nil {
|
|
writeError(w, http.StatusNotFound, "proceeding type not found")
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"proceeding_type": pt,
|
|
"timeline": timeline,
|
|
})
|
|
}
|
|
|
|
// Determine handles POST /api/deadlines/determine
|
|
// Calculates the full timeline with cascading dates and conditional logic
|
|
func (h *DetermineHandlers) Determine(w http.ResponseWriter, r *http.Request) {
|
|
var req services.DetermineRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid request body")
|
|
return
|
|
}
|
|
|
|
if req.ProceedingType == "" || req.TriggerEventDate == "" {
|
|
writeError(w, http.StatusBadRequest, "proceeding_type and trigger_event_date are required")
|
|
return
|
|
}
|
|
|
|
resp, err := h.determine.Determine(req)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, resp)
|
|
}
|
|
|
|
// BatchCreate handles POST /api/cases/{caseID}/deadlines/batch
|
|
// Creates multiple deadlines on a case from determined timeline
|
|
func (h *DetermineHandlers) BatchCreate(w http.ResponseWriter, r *http.Request) {
|
|
tenantID, ok := auth.TenantFromContext(r.Context())
|
|
if !ok {
|
|
writeError(w, http.StatusForbidden, "missing tenant")
|
|
return
|
|
}
|
|
|
|
caseID, err := parsePathUUID(r, "caseID")
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid case ID")
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
Deadlines []struct {
|
|
Title string `json:"title"`
|
|
DueDate string `json:"due_date"`
|
|
OriginalDueDate *string `json:"original_due_date,omitempty"`
|
|
RuleID *uuid.UUID `json:"rule_id,omitempty"`
|
|
RuleCode *string `json:"rule_code,omitempty"`
|
|
Notes *string `json:"notes,omitempty"`
|
|
} `json:"deadlines"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid request body")
|
|
return
|
|
}
|
|
|
|
if len(req.Deadlines) == 0 {
|
|
writeError(w, http.StatusBadRequest, "at least one deadline is required")
|
|
return
|
|
}
|
|
|
|
var created int
|
|
for _, d := range req.Deadlines {
|
|
if d.Title == "" || d.DueDate == "" {
|
|
continue
|
|
}
|
|
input := services.CreateDeadlineInput{
|
|
CaseID: caseID,
|
|
Title: d.Title,
|
|
DueDate: d.DueDate,
|
|
Source: "determined",
|
|
RuleID: d.RuleID,
|
|
Notes: d.Notes,
|
|
}
|
|
_, err := h.deadlines.Create(r.Context(), tenantID, input)
|
|
if err != nil {
|
|
internalError(w, "failed to create deadline", err)
|
|
return
|
|
}
|
|
created++
|
|
}
|
|
|
|
writeJSON(w, http.StatusCreated, map[string]any{
|
|
"created": created,
|
|
})
|
|
}
|