fix(disputed): add admin role + contact info in discussion modal
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
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:
@@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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">
|
||||||
</p>
|
{discussResult?.role} has been notified that you want to discuss.
|
||||||
<p className="text-xs text-gray-500">
|
</p>
|
||||||
Reach out directly via your team's communication channels to resolve this.
|
</div>
|
||||||
|
</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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user