diff --git a/internal/handlers/approvals.go b/internal/handlers/approvals.go index 0794015..1d9f5c3 100644 --- a/internal/handlers/approvals.go +++ b/internal/handlers/approvals.go @@ -270,7 +270,8 @@ func isValidInboxStatus(s string) bool { services.RequestStatusApproved, services.RequestStatusRejected, services.RequestStatusRevoked, - services.RequestStatusSuperseded: + services.RequestStatusSuperseded, + services.RequestStatusChangesRequested: return true } return false diff --git a/internal/handlers/approvals_test.go b/internal/handlers/approvals_test.go index 69429e6..c5676e2 100644 --- a/internal/handlers/approvals_test.go +++ b/internal/handlers/approvals_test.go @@ -135,6 +135,7 @@ func TestParseInboxFilter_DropsUnknownStatus(t *testing.T) { {"rejected", "rejected"}, {"revoked", "revoked"}, {"superseded", "superseded"}, + {"changes_requested", "changes_requested"}, // t-paliad-216 {"foo", ""}, // unknown — dropped {"DROP+TABLE", ""}, // hostile — dropped {"PENDING", ""}, // case mismatch — dropped (we don't normalise) diff --git a/internal/services/approval_service.go b/internal/services/approval_service.go index c169b3d..50695f4 100644 --- a/internal/services/approval_service.go +++ b/internal/services/approval_service.go @@ -1080,14 +1080,20 @@ func marshalJSONOrNull(m map[string]any) ([]byte, error) { // server would reject, replacing the previous click-then-alert UX. type ApprovalRequestView struct { models.ApprovalRequest - ProjectTitle string `db:"project_title" json:"project_title"` - EntityTitle *string `db:"entity_title" json:"entity_title,omitempty"` - RequesterName string `db:"requester_name" json:"requester_name"` - RequesterEmail string `db:"requester_email" json:"requester_email"` - DeciderName *string `db:"decider_name" json:"decider_name,omitempty"` - DeciderEmail *string `db:"decider_email" json:"decider_email,omitempty"` - ViewerCanApprove bool `db:"viewer_can_approve" json:"viewer_can_approve"` - ViewerIsRequester bool `db:"viewer_is_requester" json:"viewer_is_requester"` + ProjectTitle string `db:"project_title" json:"project_title"` + EntityTitle *string `db:"entity_title" json:"entity_title,omitempty"` + RequesterName string `db:"requester_name" json:"requester_name"` + RequesterEmail string `db:"requester_email" json:"requester_email"` + DeciderName *string `db:"decider_name" json:"decider_name,omitempty"` + DeciderEmail *string `db:"decider_email" json:"decider_email,omitempty"` + ViewerCanApprove bool `db:"viewer_can_approve" json:"viewer_can_approve"` + ViewerIsRequester bool `db:"viewer_is_requester" json:"viewer_is_requester"` + // NextRequestID is the forward-pointer from a changes_requested row + // to the new pending row spawned by SuggestChanges (t-paliad-216). + // Hydrated via correlated subquery on previous_request_id; the + // partial index approval_requests_previous_idx keeps the lookup O(1). + // NULL on every row that hasn't been counter-proposed. + NextRequestID *uuid.UUID `db:"next_request_id" json:"next_request_id,omitempty"` } // approvalEligibilitySQL is the SELECT-and-WHERE-compatible boolean @@ -1150,7 +1156,11 @@ const approvalRequestViewColumns = ` du.display_name AS decider_name, du.email AS decider_email, (ar.status = 'pending' AND ar.requested_by <> $1 AND ` + approvalEligibilitySQL + `) AS viewer_can_approve, - (ar.requested_by = $1) AS viewer_is_requester` + (ar.requested_by = $1) AS viewer_is_requester, + (SELECT nxt.id FROM paliad.approval_requests nxt + WHERE nxt.previous_request_id = ar.id + ORDER BY nxt.requested_at DESC + LIMIT 1) AS next_request_id` const approvalRequestViewJoins = ` paliad.approval_requests ar diff --git a/internal/services/filter_spec.go b/internal/services/filter_spec.go index e154256..e90d5bd 100644 --- a/internal/services/filter_spec.go +++ b/internal/services/filter_spec.go @@ -203,7 +203,7 @@ var KnownProjectEventKinds = []string{ // filters and request-side status filters respectively. var ( validEntityApprovalStatuses = []string{"approved", "pending", "legacy"} - validRequestStatuses = []string{"pending", "approved", "rejected", "revoked"} + validRequestStatuses = []string{"pending", "approved", "rejected", "revoked", "changes_requested"} validApprovalEntityTypes = []string{"deadline", "appointment"} validApprovalViewerRoles = []string{"approver_eligible", "self_requested", "any_visible"} validDeadlineStatuses = []string{"pending", "completed"} diff --git a/internal/services/view_service.go b/internal/services/view_service.go index 29c37a6..be4d08d 100644 --- a/internal/services/view_service.go +++ b/internal/services/view_service.go @@ -569,6 +569,11 @@ func approvalRowSubtitle(r ApprovalRequestView) string { return "Abgelehnt" case "revoked": return "Widerrufen" + case "changes_requested": + if r.DeciderName != nil { + return fmt.Sprintf("Abgelehnt mit Vorschlag von %s", *r.DeciderName) + } + return "Abgelehnt mit Vorschlag" } return r.Status }