diff --git a/backend/app/api/v1/assignments.py b/backend/app/api/v1/assignments.py index f149de5..d9dbdb2 100644 --- a/backend/app/api/v1/assignments.py +++ b/backend/app/api/v1/assignments.py @@ -1,7 +1,7 @@ """ 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.responses import Response, StreamingResponse from sqlalchemy import select @@ -49,7 +49,9 @@ def build_dispute_response(dispute: Dispute, current_user_id: int) -> DisputeRes my_vote = v.vote 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( id=dispute.id, diff --git a/backend/app/api/v1/feed.py b/backend/app/api/v1/feed.py index 790990f..37faf67 100644 --- a/backend/app/api/v1/feed.py +++ b/backend/app/api/v1/feed.py @@ -3,7 +3,8 @@ from sqlalchemy import select, func from sqlalchemy.orm import selectinload 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 router = APIRouter(tags=["feed"]) @@ -44,16 +45,40 @@ async def get_feed( ) activities = result.scalars().all() - items = [ - ActivityResponse( - id=a.id, - type=a.type, - user=UserPublic.model_validate(a.user), - data=a.data, - created_at=a.created_at, + # 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( + id=a.id, + type=a.type, + user=UserPublic.model_validate(a.user), + data=data if data else None, + created_at=a.created_at, + ) ) - for a in activities - ] return FeedResponse( items=items, diff --git a/frontend/src/components/ActivityFeed.tsx b/frontend/src/components/ActivityFeed.tsx index 790a34e..8036382 100644 --- a/frontend/src/components/ActivityFeed.tsx +++ b/frontend/src/components/ActivityFeed.tsx @@ -2,7 +2,7 @@ import { useState, useEffect, useCallback, useRef, useImperativeHandle, forwardR import { useNavigate } from 'react-router-dom' import { feedApi } from '@/api' import type { Activity, ActivityType } from '@/types' -import { Loader2, ChevronDown, Bell, ExternalLink } from 'lucide-react' +import { Loader2, ChevronDown, Bell, ExternalLink, AlertTriangle } from 'lucide-react' import { formatRelativeTime, getActivityIcon, @@ -177,9 +177,13 @@ function ActivityItem({ activity }: ActivityItemProps) { const isEvent = isEventActivity(activity.type) const { title, details, extra } = formatActivityMessage(activity) - // Get assignment_id for complete activities - const assignmentId = activity.type === 'complete' && activity.data - ? (activity.data as { assignment_id?: number }).assignment_id + // Get assignment_id and dispute status for complete activities + const activityData = activity.data as { assignment_id?: number; dispute_status?: string } | null + const assignmentId = activity.type === 'complete' && activityData?.assignment_id + ? activityData.assignment_id + : null + const disputeStatus = activity.type === 'complete' && activityData?.dispute_status + ? activityData.dispute_status : null if (isEvent) { @@ -247,15 +251,29 @@ function ActivityItem({ activity }: ActivityItemProps) { {extra} )} - {/* Details button for complete activities */} + {/* Details button and dispute indicator for complete activities */} {assignmentId && ( - +
+ + {disputeStatus === 'open' && ( + + + Оспаривается + + )} + {disputeStatus === 'valid' && ( + + + Отклонено + + )} +
)}