Fix dispute

This commit is contained in:
2025-12-16 02:35:59 +07:00
parent f57a2ba9ea
commit e32df4d95e
3 changed files with 69 additions and 24 deletions

View File

@@ -1,7 +1,7 @@
""" """
Assignment details and dispute system endpoints. Assignment details and dispute system endpoints.
""" """
from datetime import datetime, timedelta from datetime import datetime, timedelta, timezone
from fastapi import APIRouter, HTTPException, Request from fastapi import APIRouter, HTTPException, Request
from fastapi.responses import Response, StreamingResponse from fastapi.responses import Response, StreamingResponse
from sqlalchemy import select from sqlalchemy import select
@@ -49,7 +49,9 @@ def build_dispute_response(dispute: Dispute, current_user_id: int) -> DisputeRes
my_vote = v.vote my_vote = v.vote
break break
expires_at = dispute.created_at + timedelta(hours=DISPUTE_WINDOW_HOURS) # Ensure expires_at has UTC timezone info for correct frontend parsing
created_at_utc = dispute.created_at.replace(tzinfo=timezone.utc) if dispute.created_at.tzinfo is None else dispute.created_at
expires_at = created_at_utc + timedelta(hours=DISPUTE_WINDOW_HOURS)
return DisputeResponse( return DisputeResponse(
id=dispute.id, id=dispute.id,

View File

@@ -3,7 +3,8 @@ from sqlalchemy import select, func
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
from app.api.deps import DbSession, CurrentUser from app.api.deps import DbSession, CurrentUser
from app.models import Activity, Participant from app.models import Activity, Participant, Dispute, ActivityType
from app.models.dispute import DisputeStatus
from app.schemas import FeedResponse, ActivityResponse, UserPublic from app.schemas import FeedResponse, ActivityResponse, UserPublic
router = APIRouter(tags=["feed"]) router = APIRouter(tags=["feed"])
@@ -44,16 +45,40 @@ async def get_feed(
) )
activities = result.scalars().all() activities = result.scalars().all()
items = [ # Get assignment_ids from complete activities to check for disputes
complete_assignment_ids = []
for a in activities:
if a.type == ActivityType.COMPLETE.value and a.data and a.data.get("assignment_id"):
complete_assignment_ids.append(a.data["assignment_id"])
# Get disputes for these assignments
disputes_map: dict[int, str] = {}
if complete_assignment_ids:
result = await db.execute(
select(Dispute).where(Dispute.assignment_id.in_(complete_assignment_ids))
)
for dispute in result.scalars().all():
disputes_map[dispute.assignment_id] = dispute.status
items = []
for a in activities:
data = dict(a.data) if a.data else {}
# Add dispute status to complete activities
if a.type == ActivityType.COMPLETE.value and a.data and a.data.get("assignment_id"):
assignment_id = a.data["assignment_id"]
if assignment_id in disputes_map:
data["dispute_status"] = disputes_map[assignment_id]
items.append(
ActivityResponse( ActivityResponse(
id=a.id, id=a.id,
type=a.type, type=a.type,
user=UserPublic.model_validate(a.user), user=UserPublic.model_validate(a.user),
data=a.data, data=data if data else None,
created_at=a.created_at, created_at=a.created_at,
) )
for a in activities )
]
return FeedResponse( return FeedResponse(
items=items, items=items,

View File

@@ -2,7 +2,7 @@ import { useState, useEffect, useCallback, useRef, useImperativeHandle, forwardR
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { feedApi } from '@/api' import { feedApi } from '@/api'
import type { Activity, ActivityType } from '@/types' import type { Activity, ActivityType } from '@/types'
import { Loader2, ChevronDown, Bell, ExternalLink } from 'lucide-react' import { Loader2, ChevronDown, Bell, ExternalLink, AlertTriangle } from 'lucide-react'
import { import {
formatRelativeTime, formatRelativeTime,
getActivityIcon, getActivityIcon,
@@ -177,9 +177,13 @@ function ActivityItem({ activity }: ActivityItemProps) {
const isEvent = isEventActivity(activity.type) const isEvent = isEventActivity(activity.type)
const { title, details, extra } = formatActivityMessage(activity) const { title, details, extra } = formatActivityMessage(activity)
// Get assignment_id for complete activities // Get assignment_id and dispute status for complete activities
const assignmentId = activity.type === 'complete' && activity.data const activityData = activity.data as { assignment_id?: number; dispute_status?: string } | null
? (activity.data as { assignment_id?: number }).assignment_id const assignmentId = activity.type === 'complete' && activityData?.assignment_id
? activityData.assignment_id
: null
const disputeStatus = activity.type === 'complete' && activityData?.dispute_status
? activityData.dispute_status
: null : null
if (isEvent) { if (isEvent) {
@@ -247,15 +251,29 @@ function ActivityItem({ activity }: ActivityItemProps) {
{extra} {extra}
</div> </div>
)} )}
{/* Details button for complete activities */} {/* Details button and dispute indicator for complete activities */}
{assignmentId && ( {assignmentId && (
<div className="flex items-center gap-3 mt-2">
<button <button
onClick={() => navigate(`/assignments/${assignmentId}`)} onClick={() => navigate(`/assignments/${assignmentId}`)}
className="mt-2 text-xs text-primary-400 hover:text-primary-300 flex items-center gap-1" className="text-xs text-primary-400 hover:text-primary-300 flex items-center gap-1"
> >
<ExternalLink className="w-3 h-3" /> <ExternalLink className="w-3 h-3" />
Детали Детали
</button> </button>
{disputeStatus === 'open' && (
<span className="text-xs text-orange-400 flex items-center gap-1">
<AlertTriangle className="w-3 h-3" />
Оспаривается
</span>
)}
{disputeStatus === 'valid' && (
<span className="text-xs text-red-400 flex items-center gap-1">
<AlertTriangle className="w-3 h-3" />
Отклонено
</span>
)}
</div>
)} )}
</div> </div>
</div> </div>