From 02ff89401c22cf654904363987bb74b0f90e3487 Mon Sep 17 00:00:00 2001 From: kitos Date: Wed, 3 Jun 2026 13:02:57 +0200 Subject: [PATCH] fix(disputed): add admin role + contact info in discussion modal - request-discussion endpoint: add 'admin' to allowed roles - Return rejector_email and rejector_role in the response - Modal success state shows contact card with username, role, email link so the approving lead can immediately reach out to the rejecting lead --- backend/app/routers/tests.py | 13 +++-- frontend/src/api/tests.ts | 2 + .../test-detail/TestDetailHeader.tsx | 54 ++++++++++++++----- 3 files changed, 52 insertions(+), 17 deletions(-) diff --git a/backend/app/routers/tests.py b/backend/app/routers/tests.py index f6c2bc5..67eb61a 100644 --- a/backend/app/routers/tests.py +++ b/backend/app/routers/tests.py @@ -754,7 +754,7 @@ def sync_tempo( def request_discussion( test_id: uuid.UUID, db: Session = Depends(get_db), - current_user: User = Depends(require_any_role("red_lead", "blue_lead")), + current_user: User = Depends(require_any_role("red_lead", "blue_lead", "admin")), ): """Called when the approving lead confirms their vote in a disputed test. @@ -774,12 +774,12 @@ def request_discussion( role = current_user.role # Identify who the "other lead" is (the one who rejected) - if role == "red_lead" and test.red_validation_status == "approved": + if (role in ("red_lead", "admin")) and test.red_validation_status == "approved": # Red approved, Blue rejected → notify Blue Lead who rejected rejector_id = test.blue_validated_by rejector_label = "Blue Lead" requester_label = "Red Lead" - elif role == "blue_lead" and test.blue_validation_status == "approved": + elif (role in ("blue_lead", "admin")) and test.blue_validation_status == "approved": # Blue approved, Red rejected → notify Red Lead who rejected rejector_id = test.red_validated_by rejector_label = "Red Lead" @@ -787,15 +787,16 @@ def request_discussion( else: from app.domain.errors import BusinessRuleViolation raise BusinessRuleViolation( - "You are not the approving lead in this conflict or the state is inconsistent" + "The conflict state is inconsistent — no approving lead found" ) - # Look up the rejecting lead's username for the message + # Look up the rejecting lead's full info for the response rejector = ( db.query(UserModel).filter(UserModel.id == rejector_id).first() if rejector_id else None ) rejector_name = rejector.username if rejector else rejector_label + rejector_email = getattr(rejector, "email", None) if rejector else None # Notify the rejecting lead if rejector_id: @@ -833,6 +834,8 @@ def request_discussion( "status": "notification_sent", "message": f"Discussion request sent to {rejector_name}", "rejector_username": rejector_name, + "rejector_email": rejector_email, + "rejector_role": rejector_label, } diff --git a/frontend/src/api/tests.ts b/frontend/src/api/tests.ts index f40716a..243e89e 100644 --- a/frontend/src/api/tests.ts +++ b/frontend/src/api/tests.ts @@ -313,6 +313,8 @@ export async function requestDiscussion(testId: string): Promise<{ status: string; message: string; rejector_username: string; + rejector_email: string | null; + rejector_role: string; }> { const { data } = await client.post(`/tests/${testId}/request-discussion`); return data; diff --git a/frontend/src/components/test-detail/TestDetailHeader.tsx b/frontend/src/components/test-detail/TestDetailHeader.tsx index 4d233c0..26b58f7 100644 --- a/frontend/src/components/test-detail/TestDetailHeader.tsx +++ b/frontend/src/components/test-detail/TestDetailHeader.tsx @@ -87,11 +87,17 @@ export default function TestDetailHeader({ const role = user?.role ?? ""; const currentIdx = STATE_INDEX[test.state]; const [showDiscussModal, setShowDiscussModal] = useState(false); - const [discussResult, setDiscussResult] = useState(null); + const [discussResult, setDiscussResult] = useState<{ + username: string; email: string | null; role: string; + } | null>(null); const discussMutation = useMutation({ mutationFn: () => requestDiscussion(test.id), - onSuccess: (data) => setDiscussResult(data.rejector_username), + onSuccess: (data) => setDiscussResult({ + username: data.rejector_username, + email: data.rejector_email, + role: data.rejector_role, + }), }); const formatDate = (d: string | null) => { @@ -544,16 +550,40 @@ export default function TestDetailHeader({ )} ) : ( - /* Success state */ -
- -

Discussion request sent

-

- {discussResult} has been notified - that you are standing by your approval and want to discuss their rejection. -

-

- Reach out directly via your team's communication channels to resolve this. + /* Success state with contact info */ +

+
+ +
+

Discussion request sent

+

+ {discussResult?.role} has been notified that you want to discuss. +

+
+
+ + {/* Contact card */} +
+

Contact details

+
+ {discussResult?.username} + + {discussResult?.role} + +
+ {discussResult?.email && ( + + ✉ {discussResult.email} + + )} +
+ +

+ Reach out directly via the contact above or your team's communication channels (Slack, Teams, etc.) to resolve the disagreement. + The test will remain in Disputed state until one lead changes their vote.

)}