feat: UPC deadline determination — event-driven model with proceeding timeline

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.
This commit is contained in:
m
2026-03-30 11:33:59 +02:00
parent 8e65463130
commit a89ef26ebd
14 changed files with 1642 additions and 171 deletions

View File

@@ -8,6 +8,12 @@ import (
"mgit.msbls.de/m/KanzlAI-mGMT/internal/models"
)
const ruleColumns = `id, proceeding_type_id, parent_id, code, name, description,
primary_party, event_type, is_mandatory, duration_value, duration_unit,
timing, rule_code, deadline_notes, sequence_order, condition_rule_id,
alt_duration_value, alt_duration_unit, alt_rule_code,
is_spawn, spawn_label, is_active, created_at, updated_at`
// DeadlineRuleService handles deadline rule queries
type DeadlineRuleService struct {
db *sqlx.DB
@@ -25,21 +31,13 @@ func (s *DeadlineRuleService) List(proceedingTypeID *int) ([]models.DeadlineRule
if proceedingTypeID != nil {
err = s.db.Select(&rules,
`SELECT id, proceeding_type_id, parent_id, code, name, description,
primary_party, event_type, is_mandatory, duration_value, duration_unit,
timing, rule_code, deadline_notes, sequence_order, condition_rule_id,
alt_duration_value, alt_duration_unit, alt_rule_code, is_active,
created_at, updated_at
`SELECT `+ruleColumns+`
FROM deadline_rules
WHERE proceeding_type_id = $1 AND is_active = true
ORDER BY sequence_order`, *proceedingTypeID)
} else {
err = s.db.Select(&rules,
`SELECT id, proceeding_type_id, parent_id, code, name, description,
primary_party, event_type, is_mandatory, duration_value, duration_unit,
timing, rule_code, deadline_notes, sequence_order, condition_rule_id,
alt_duration_value, alt_duration_unit, alt_rule_code, is_active,
created_at, updated_at
`SELECT `+ruleColumns+`
FROM deadline_rules
WHERE is_active = true
ORDER BY proceeding_type_id, sequence_order`)
@@ -72,11 +70,7 @@ func (s *DeadlineRuleService) GetRuleTree(proceedingTypeCode string) ([]RuleTree
// Get all rules for this proceeding type
var rules []models.DeadlineRule
err = s.db.Select(&rules,
`SELECT id, proceeding_type_id, parent_id, code, name, description,
primary_party, event_type, is_mandatory, duration_value, duration_unit,
timing, rule_code, deadline_notes, sequence_order, condition_rule_id,
alt_duration_value, alt_duration_unit, alt_rule_code, is_active,
created_at, updated_at
`SELECT `+ruleColumns+`
FROM deadline_rules
WHERE proceeding_type_id = $1 AND is_active = true
ORDER BY sequence_order`, pt.ID)
@@ -87,6 +81,36 @@ func (s *DeadlineRuleService) GetRuleTree(proceedingTypeCode string) ([]RuleTree
return buildTree(rules), nil
}
// GetFullTimeline returns the full event tree for a proceeding type using a recursive CTE.
// Unlike GetRuleTree, this follows parent_id across proceeding types (includes cross-type spawns).
func (s *DeadlineRuleService) GetFullTimeline(proceedingTypeCode string) ([]models.DeadlineRule, *models.ProceedingType, error) {
var pt models.ProceedingType
err := s.db.Get(&pt,
`SELECT id, code, name, description, jurisdiction, default_color, sort_order, is_active
FROM proceeding_types
WHERE code = $1 AND is_active = true`, proceedingTypeCode)
if err != nil {
return nil, nil, fmt.Errorf("resolving proceeding type %q: %w", proceedingTypeCode, err)
}
var rules []models.DeadlineRule
err = s.db.Select(&rules, `
WITH RECURSIVE tree AS (
SELECT * FROM deadline_rules
WHERE proceeding_type_id = $1 AND parent_id IS NULL AND is_active = true
UNION ALL
SELECT dr.* FROM deadline_rules dr
JOIN tree t ON dr.parent_id = t.id
WHERE dr.is_active = true
)
SELECT `+ruleColumns+` FROM tree ORDER BY sequence_order`, pt.ID)
if err != nil {
return nil, nil, fmt.Errorf("fetching timeline for type %q: %w", proceedingTypeCode, err)
}
return rules, &pt, nil
}
// GetByIDs returns deadline rules by their IDs
func (s *DeadlineRuleService) GetByIDs(ids []string) ([]models.DeadlineRule, error) {
if len(ids) == 0 {
@@ -94,11 +118,7 @@ func (s *DeadlineRuleService) GetByIDs(ids []string) ([]models.DeadlineRule, err
}
query, args, err := sqlx.In(
`SELECT id, proceeding_type_id, parent_id, code, name, description,
primary_party, event_type, is_mandatory, duration_value, duration_unit,
timing, rule_code, deadline_notes, sequence_order, condition_rule_id,
alt_duration_value, alt_duration_unit, alt_rule_code, is_active,
created_at, updated_at
`SELECT `+ruleColumns+`
FROM deadline_rules
WHERE id IN (?) AND is_active = true
ORDER BY sequence_order`, ids)
@@ -119,11 +139,7 @@ func (s *DeadlineRuleService) GetByIDs(ids []string) ([]models.DeadlineRule, err
func (s *DeadlineRuleService) GetRulesForProceedingType(proceedingTypeID int) ([]models.DeadlineRule, error) {
var rules []models.DeadlineRule
err := s.db.Select(&rules,
`SELECT id, proceeding_type_id, parent_id, code, name, description,
primary_party, event_type, is_mandatory, duration_value, duration_unit,
timing, rule_code, deadline_notes, sequence_order, condition_rule_id,
alt_duration_value, alt_duration_unit, alt_rule_code, is_active,
created_at, updated_at
`SELECT `+ruleColumns+`
FROM deadline_rules
WHERE proceeding_type_id = $1 AND is_active = true
ORDER BY sequence_order`, proceedingTypeID)