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.
192 lines
5.8 KiB
Go
192 lines
5.8 KiB
Go
package services
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/jmoiron/sqlx"
|
|
|
|
"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
|
|
}
|
|
|
|
// NewDeadlineRuleService creates a new deadline rule service
|
|
func NewDeadlineRuleService(db *sqlx.DB) *DeadlineRuleService {
|
|
return &DeadlineRuleService{db: db}
|
|
}
|
|
|
|
// List returns deadline rules, optionally filtered by proceeding type
|
|
func (s *DeadlineRuleService) List(proceedingTypeID *int) ([]models.DeadlineRule, error) {
|
|
var rules []models.DeadlineRule
|
|
var err error
|
|
|
|
if proceedingTypeID != nil {
|
|
err = s.db.Select(&rules,
|
|
`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 `+ruleColumns+`
|
|
FROM deadline_rules
|
|
WHERE is_active = true
|
|
ORDER BY proceeding_type_id, sequence_order`)
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("listing deadline rules: %w", err)
|
|
}
|
|
return rules, nil
|
|
}
|
|
|
|
// RuleTreeNode represents a deadline rule with its children
|
|
type RuleTreeNode struct {
|
|
models.DeadlineRule
|
|
Children []RuleTreeNode `json:"children,omitempty"`
|
|
}
|
|
|
|
// GetRuleTree returns a hierarchical tree of rules for a proceeding type
|
|
func (s *DeadlineRuleService) GetRuleTree(proceedingTypeCode string) ([]RuleTreeNode, error) {
|
|
// First resolve proceeding type code to ID
|
|
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, fmt.Errorf("resolving proceeding type %q: %w", proceedingTypeCode, err)
|
|
}
|
|
|
|
// Get all rules for this proceeding type
|
|
var rules []models.DeadlineRule
|
|
err = s.db.Select(&rules,
|
|
`SELECT `+ruleColumns+`
|
|
FROM deadline_rules
|
|
WHERE proceeding_type_id = $1 AND is_active = true
|
|
ORDER BY sequence_order`, pt.ID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("listing rules for type %q: %w", proceedingTypeCode, err)
|
|
}
|
|
|
|
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 {
|
|
return nil, nil
|
|
}
|
|
|
|
query, args, err := sqlx.In(
|
|
`SELECT `+ruleColumns+`
|
|
FROM deadline_rules
|
|
WHERE id IN (?) AND is_active = true
|
|
ORDER BY sequence_order`, ids)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("building IN query: %w", err)
|
|
}
|
|
query = s.db.Rebind(query)
|
|
|
|
var rules []models.DeadlineRule
|
|
err = s.db.Select(&rules, query, args...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("fetching rules by IDs: %w", err)
|
|
}
|
|
return rules, nil
|
|
}
|
|
|
|
// GetRulesForProceedingType returns all active rules for a proceeding type ID
|
|
func (s *DeadlineRuleService) GetRulesForProceedingType(proceedingTypeID int) ([]models.DeadlineRule, error) {
|
|
var rules []models.DeadlineRule
|
|
err := s.db.Select(&rules,
|
|
`SELECT `+ruleColumns+`
|
|
FROM deadline_rules
|
|
WHERE proceeding_type_id = $1 AND is_active = true
|
|
ORDER BY sequence_order`, proceedingTypeID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("listing rules for proceeding type %d: %w", proceedingTypeID, err)
|
|
}
|
|
return rules, nil
|
|
}
|
|
|
|
// ListProceedingTypes returns all active proceeding types
|
|
func (s *DeadlineRuleService) ListProceedingTypes() ([]models.ProceedingType, error) {
|
|
var types []models.ProceedingType
|
|
err := s.db.Select(&types,
|
|
`SELECT id, code, name, description, jurisdiction, default_color, sort_order, is_active
|
|
FROM proceeding_types
|
|
WHERE is_active = true
|
|
ORDER BY sort_order`)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("listing proceeding types: %w", err)
|
|
}
|
|
return types, nil
|
|
}
|
|
|
|
// buildTree converts a flat list of rules into a hierarchical tree
|
|
func buildTree(rules []models.DeadlineRule) []RuleTreeNode {
|
|
nodeMap := make(map[string]*RuleTreeNode, len(rules))
|
|
var roots []RuleTreeNode
|
|
|
|
// Create nodes
|
|
for _, r := range rules {
|
|
node := RuleTreeNode{DeadlineRule: r}
|
|
nodeMap[r.ID.String()] = &node
|
|
}
|
|
|
|
// Build tree
|
|
for _, r := range rules {
|
|
node := nodeMap[r.ID.String()]
|
|
if r.ParentID != nil {
|
|
parentKey := r.ParentID.String()
|
|
if parent, ok := nodeMap[parentKey]; ok {
|
|
parent.Children = append(parent.Children, *node)
|
|
continue
|
|
}
|
|
}
|
|
roots = append(roots, *node)
|
|
}
|
|
|
|
return roots
|
|
}
|