fix(disputed): add admin role + contact info in discussion modal
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled

- 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
This commit is contained in:
kitos
2026-06-03 13:02:57 +02:00
parent 4e20bfa835
commit 02ff89401c
3 changed files with 52 additions and 17 deletions

View File

@@ -754,7 +754,7 @@ def sync_tempo(
def request_discussion( def request_discussion(
test_id: uuid.UUID, test_id: uuid.UUID,
db: Session = Depends(get_db), 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. """Called when the approving lead confirms their vote in a disputed test.
@@ -774,12 +774,12 @@ def request_discussion(
role = current_user.role role = current_user.role
# Identify who the "other lead" is (the one who rejected) # 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 # Red approved, Blue rejected → notify Blue Lead who rejected
rejector_id = test.blue_validated_by rejector_id = test.blue_validated_by
rejector_label = "Blue Lead" rejector_label = "Blue Lead"
requester_label = "Red 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 # Blue approved, Red rejected → notify Red Lead who rejected
rejector_id = test.red_validated_by rejector_id = test.red_validated_by
rejector_label = "Red Lead" rejector_label = "Red Lead"
@@ -787,15 +787,16 @@ def request_discussion(
else: else:
from app.domain.errors import BusinessRuleViolation from app.domain.errors import BusinessRuleViolation
raise 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 = ( rejector = (
db.query(UserModel).filter(UserModel.id == rejector_id).first() db.query(UserModel).filter(UserModel.id == rejector_id).first()
if rejector_id else None if rejector_id else None
) )
rejector_name = rejector.username if rejector else rejector_label rejector_name = rejector.username if rejector else rejector_label
rejector_email = getattr(rejector, "email", None) if rejector else None
# Notify the rejecting lead # Notify the rejecting lead
if rejector_id: if rejector_id:
@@ -833,6 +834,8 @@ def request_discussion(
"status": "notification_sent", "status": "notification_sent",
"message": f"Discussion request sent to {rejector_name}", "message": f"Discussion request sent to {rejector_name}",
"rejector_username": rejector_name, "rejector_username": rejector_name,
"rejector_email": rejector_email,
"rejector_role": rejector_label,
} }

View File

@@ -313,6 +313,8 @@ export async function requestDiscussion(testId: string): Promise<{
status: string; status: string;
message: string; message: string;
rejector_username: string; rejector_username: string;
rejector_email: string | null;
rejector_role: string;
}> { }> {
const { data } = await client.post(`/tests/${testId}/request-discussion`); const { data } = await client.post(`/tests/${testId}/request-discussion`);
return data; return data;

View File

@@ -87,11 +87,17 @@ export default function TestDetailHeader({
const role = user?.role ?? ""; const role = user?.role ?? "";
const currentIdx = STATE_INDEX[test.state]; const currentIdx = STATE_INDEX[test.state];
const [showDiscussModal, setShowDiscussModal] = useState(false); const [showDiscussModal, setShowDiscussModal] = useState(false);
const [discussResult, setDiscussResult] = useState<string | null>(null); const [discussResult, setDiscussResult] = useState<{
username: string; email: string | null; role: string;
} | null>(null);
const discussMutation = useMutation({ const discussMutation = useMutation({
mutationFn: () => requestDiscussion(test.id), 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) => { const formatDate = (d: string | null) => {
@@ -544,16 +550,40 @@ export default function TestDetailHeader({
)} )}
</div> </div>
) : ( ) : (
/* Success state */ /* Success state with contact info */
<div className="px-6 py-8 text-center space-y-3"> <div className="px-6 py-6 space-y-4">
<CheckCircle className="mx-auto h-12 w-12 text-green-400" /> <div className="flex items-center gap-3">
<p className="text-lg font-semibold text-white">Discussion request sent</p> <CheckCircle className="h-8 w-8 shrink-0 text-green-400" />
<p className="text-sm text-gray-400"> <div>
<strong className="text-white">{discussResult}</strong> has been notified <p className="text-base font-semibold text-white">Discussion request sent</p>
that you are standing by your approval and want to discuss their rejection. <p className="text-xs text-gray-400">
{discussResult?.role} has been notified that you want to discuss.
</p> </p>
<p className="text-xs text-gray-500"> </div>
Reach out directly via your team's communication channels to resolve this. </div>
{/* Contact card */}
<div className="rounded-lg border border-amber-500/20 bg-amber-500/5 p-4 space-y-2">
<p className="text-xs font-semibold uppercase tracking-wider text-amber-400">Contact details</p>
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-white">{discussResult?.username}</span>
<span className="rounded-full border border-gray-600 bg-gray-800 px-2 py-0.5 text-[10px] text-gray-400">
{discussResult?.role}
</span>
</div>
{discussResult?.email && (
<a
href={`mailto:${discussResult.email}`}
className="flex items-center gap-1.5 text-xs text-cyan-400 hover:underline"
>
{discussResult.email}
</a>
)}
</div>
<p className="text-xs text-gray-500 leading-relaxed">
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 <strong className="text-amber-400">Disputed</strong> state until one lead changes their vote.
</p> </p>
</div> </div>
)} )}