Two additive endpoints behind the Fristenrechner overhaul (design §6.1 + §6.2 in docs/design-fristenrechner-overhaul-2026-05-26.md): 1. GET /api/tools/fristenrechner/search?kind=events — returns procedural_events rows directly (not aggregated concept-cards), one hit per (event × proceeding_type) tuple. Trigram-ranked against name / name_en / code. Filters: jurisdiction, proc, event_kind, party. Powers Mode A's result list and Mode B's R4 landing chips. Default search shape unchanged. 2. GET /api/tools/fristenrechner/follow-ups?event=...&trigger_date=... — given a trigger event (by code or uuid) + date, returns the immediate follow-up sequencing rules with computed due dates via litigationplanner.CalculateRule. Each row carries priority / primary_party / is_court_set / is_spawn / has_condition / legal source / spawn target so the result view can group into Mandatory / Recommended / Optional / Conditional with the SPAWNED badge. party=claimant|defendant filters keep "both" rules visible. No schema changes — unified sequencing_rules already has every column needed. Live-DB tests cover the SoC follow-up shape, party narrowing, jurisdiction + event_kind filters, and the unknown- event sentinel.
130 lines
4.8 KiB
Go
130 lines
4.8 KiB
Go
package handlers
|
||
|
||
import (
|
||
"net/http"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"mgit.msbls.de/m/paliad/internal/services"
|
||
)
|
||
|
||
// GET /api/tools/fristenrechner/search — unified search across the
|
||
// Fristenrechner concept layer (t-paliad-131 Phase C, t-paliad-133 v3
|
||
// extension). Returns at most `limit` concept cards, each with its
|
||
// proceeding pills.
|
||
//
|
||
// Query params:
|
||
// q - free-text search (trigram + alias)
|
||
// party - filter by effective_party
|
||
// proc - filter by proceeding_type code
|
||
// source - filter by legal_source prefix
|
||
// event_category_slug - v3 B1 narrowing; only concepts reachable
|
||
// from this taxonomy node and its descendants
|
||
// appear. Empty q is allowed when this is set
|
||
// (browse mode).
|
||
// browse - "all" enables v3 B1 entry mode: returns
|
||
// every concept mapped to any leaf of the
|
||
// decision tree (no narrowing, no query).
|
||
// Ignored when q is non-empty.
|
||
// forum - comma-separated v3 forum-bucket slugs
|
||
// (upc_cfi, upc_coa, de_lg, de_olg, de_bgh,
|
||
// de_bpatg, epa_grant, epa_opp, epa_appeal,
|
||
// dpma). Trigger pills bypass this filter.
|
||
// limit - max cards (default 12, max 30; in browse
|
||
// modes default 200, max 500)
|
||
// kind - "events" switches to the events-shape
|
||
// response (Fristenrechner overhaul S1,
|
||
// design §6.1). The default concept-card
|
||
// shape is unchanged when kind is empty.
|
||
//
|
||
// Returns an empty cards array (not 400) when q is empty — that lets
|
||
// the frontend boot the search input without a server round-trip.
|
||
func handleFristenrechnerSearch(w http.ResponseWriter, r *http.Request) {
|
||
if dbSvc == nil || dbSvc.deadlineSearch == nil {
|
||
writeJSON(w, http.StatusServiceUnavailable, map[string]string{
|
||
"error": "Suche vorübergehend nicht verfügbar (keine Datenbank).",
|
||
})
|
||
return
|
||
}
|
||
if r.URL.Query().Get("kind") == "events" {
|
||
handleFristenrechnerSearchEvents(w, r)
|
||
return
|
||
}
|
||
q := r.URL.Query().Get("q")
|
||
opts := services.SearchOptions{
|
||
Party: r.URL.Query().Get("party"),
|
||
Proc: r.URL.Query().Get("proc"),
|
||
Source: r.URL.Query().Get("source"),
|
||
EventCategorySlug: r.URL.Query().Get("event_category_slug"),
|
||
Forums: parseCSV(r.URL.Query().Get("forum")),
|
||
BrowseAll: r.URL.Query().Get("browse") == "all",
|
||
Limit: parseLimit(r.URL.Query().Get("limit")),
|
||
}
|
||
resp, err := dbSvc.deadlineSearch.Search(r.Context(), q, opts)
|
||
if err != nil {
|
||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "Suche fehlgeschlagen: " + err.Error()})
|
||
return
|
||
}
|
||
writeJSON(w, http.StatusOK, resp)
|
||
}
|
||
|
||
// handleFristenrechnerSearchEvents serves the ?kind=events shape of
|
||
// /api/tools/fristenrechner/search (overhaul S1, design §6.1). Returns
|
||
// one hit per (procedural_event × proceeding_type) tuple, with a
|
||
// follow-up count and a trigram similarity score.
|
||
//
|
||
// Query params (additive to the legacy search params):
|
||
// q - free-text search against name / name_en / code
|
||
// jurisdiction - "UPC" | "DE" | "EPA" | "DPMA"
|
||
// proc - proceeding_type code
|
||
// event_kind - "filing" | "hearing" | "decision" | "order"
|
||
// party - primary_party of the anchor rule
|
||
// limit - max hits (default 50, max 200)
|
||
func handleFristenrechnerSearchEvents(w http.ResponseWriter, r *http.Request) {
|
||
q := r.URL.Query().Get("q")
|
||
opts := services.EventSearchOptions{
|
||
Jurisdiction: strings.ToUpper(strings.TrimSpace(r.URL.Query().Get("jurisdiction"))),
|
||
ProceedingTypeCode: r.URL.Query().Get("proc"),
|
||
EventKind: r.URL.Query().Get("event_kind"),
|
||
PrimaryParty: r.URL.Query().Get("party"),
|
||
Limit: parseLimit(r.URL.Query().Get("limit")),
|
||
}
|
||
resp, err := dbSvc.deadlineSearch.SearchEvents(r.Context(), q, opts)
|
||
if err != nil {
|
||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "Ereignis-Suche fehlgeschlagen: " + err.Error()})
|
||
return
|
||
}
|
||
writeJSON(w, http.StatusOK, resp)
|
||
}
|
||
|
||
// parseCSV splits a comma-separated query-string value into a slice of
|
||
// trimmed non-empty entries. Empty input → nil.
|
||
func parseCSV(raw string) []string {
|
||
if raw == "" {
|
||
return nil
|
||
}
|
||
parts := strings.Split(raw, ",")
|
||
out := make([]string, 0, len(parts))
|
||
for _, p := range parts {
|
||
p = strings.TrimSpace(p)
|
||
if p != "" {
|
||
out = append(out, p)
|
||
}
|
||
}
|
||
if len(out) == 0 {
|
||
return nil
|
||
}
|
||
return out
|
||
}
|
||
|
||
func parseLimit(raw string) int {
|
||
if raw == "" {
|
||
return 0
|
||
}
|
||
n, err := strconv.Atoi(raw)
|
||
if err != nil || n < 0 {
|
||
return 0
|
||
}
|
||
return n
|
||
}
|