Fix broken joinedload and import path in reviews/activities endpoints
joinedload("reviewer") used a string instead of a class-bound
attribute (rejected by SQLAlchemy 2.x), and models.submission was the
wrong import path for Submission. Both endpoints 500'd whenever
actually called; surfaced while wiring real dashboard/activity data
into the frontend.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,209 +0,0 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-lg font-semibold">Activity Timeline</h3>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@click="loadActivities"
|
||||
:disabled="loading"
|
||||
>
|
||||
<RefreshCw class="h-4 w-4" :class="{ 'animate-spin': loading }" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div v-if="loading && activities.length === 0" class="text-center py-8 text-muted-foreground">
|
||||
Loading timeline...
|
||||
</div>
|
||||
|
||||
<div v-else-if="activities.length === 0" class="text-center py-8 text-muted-foreground">
|
||||
<Clock class="h-12 w-12 mx-auto mb-2 opacity-50" />
|
||||
<p>No activity recorded</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="relative">
|
||||
<!-- Timeline line -->
|
||||
<div class="absolute left-4 top-0 bottom-0 w-0.5 bg-border" />
|
||||
|
||||
<!-- Timeline items -->
|
||||
<div class="space-y-6">
|
||||
<div
|
||||
v-for="activity in activities"
|
||||
:key="activity.id"
|
||||
class="relative pl-10"
|
||||
>
|
||||
<!-- Timeline dot -->
|
||||
<div
|
||||
class="absolute left-2.5 w-3 h-3 rounded-full border-2 border-background"
|
||||
:class="getTimelineDotColor(activity.type)"
|
||||
/>
|
||||
|
||||
<!-- Activity content -->
|
||||
<div class="bg-card border rounded-lg p-4">
|
||||
<div class="flex items-start gap-3">
|
||||
<component
|
||||
:is="getActivityIcon(activity.type)"
|
||||
class="h-5 w-5 mt-0.5 flex-shrink-0"
|
||||
:class="getActivityColor(activity.type)"
|
||||
/>
|
||||
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-medium">{{ getActivityTitle(activity.type) }}</p>
|
||||
<p class="text-sm text-muted-foreground mt-1">{{ activity.description }}</p>
|
||||
|
||||
<!-- Metadata display -->
|
||||
<div v-if="activity.activity_metadata" class="mt-2 text-xs text-muted-foreground">
|
||||
<div v-if="activity.activity_metadata.old_status && activity.activity_metadata.new_status">
|
||||
<Badge variant="outline" class="mr-2">{{ activity.activity_metadata.old_status }}</Badge>
|
||||
→
|
||||
<Badge variant="outline" class="ml-2">{{ activity.activity_metadata.new_status }}</Badge>
|
||||
</div>
|
||||
<div v-if="activity.activity_metadata.version">
|
||||
Version {{ activity.activity_metadata.version }}
|
||||
</div>
|
||||
<div v-if="activity.activity_metadata.decision">
|
||||
Decision: <Badge :variant="activity.activity_metadata.decision === 'approved' ? 'default' : 'destructive'">
|
||||
{{ activity.activity_metadata.decision }}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 mt-2">
|
||||
<Avatar class="h-5 w-5">
|
||||
<AvatarFallback class="text-xs">
|
||||
{{ getInitials(activity.user) }}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<span class="text-xs text-muted-foreground">
|
||||
{{ activity.user.first_name }} {{ activity.user.last_name }}
|
||||
</span>
|
||||
<span class="text-xs text-muted-foreground">•</span>
|
||||
<span class="text-xs text-muted-foreground">
|
||||
{{ formatTime(activity.created_at) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import {
|
||||
Clock,
|
||||
FileText,
|
||||
CheckCircle,
|
||||
UserPlus,
|
||||
MessageSquare,
|
||||
RefreshCw
|
||||
} from 'lucide-vue-next'
|
||||
import type { Activity, ActivityType, UserInfo } from '@/types/activity'
|
||||
import * as activityService from '@/services/activity'
|
||||
|
||||
interface Props {
|
||||
taskId: number
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const activities = ref<Activity[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
loadActivities()
|
||||
})
|
||||
|
||||
watch(() => props.taskId, () => {
|
||||
loadActivities()
|
||||
})
|
||||
|
||||
async function loadActivities() {
|
||||
loading.value = true
|
||||
try {
|
||||
activities.value = await activityService.getTaskActivities(props.taskId)
|
||||
} catch (error) {
|
||||
console.error('Failed to load task activities:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function getInitials(user: UserInfo): string {
|
||||
return `${user.first_name[0]}${user.last_name[0]}`.toUpperCase()
|
||||
}
|
||||
|
||||
function getActivityIcon(type: ActivityType) {
|
||||
const iconMap: Record<string, any> = {
|
||||
task_created: FileText,
|
||||
task_updated: FileText,
|
||||
task_assigned: UserPlus,
|
||||
task_status_changed: CheckCircle,
|
||||
submission_created: FileText,
|
||||
submission_reviewed: CheckCircle,
|
||||
comment_added: MessageSquare
|
||||
}
|
||||
return iconMap[type] || FileText
|
||||
}
|
||||
|
||||
function getActivityColor(type: ActivityType): string {
|
||||
const colorMap: Record<string, string> = {
|
||||
task_created: 'text-blue-500',
|
||||
task_updated: 'text-blue-500',
|
||||
task_assigned: 'text-purple-500',
|
||||
task_status_changed: 'text-green-500',
|
||||
submission_created: 'text-orange-500',
|
||||
submission_reviewed: 'text-green-500',
|
||||
comment_added: 'text-gray-500'
|
||||
}
|
||||
return colorMap[type] || 'text-gray-500'
|
||||
}
|
||||
|
||||
function getTimelineDotColor(type: ActivityType): string {
|
||||
const colorMap: Record<string, string> = {
|
||||
task_created: 'bg-blue-500',
|
||||
task_updated: 'bg-blue-500',
|
||||
task_assigned: 'bg-purple-500',
|
||||
task_status_changed: 'bg-green-500',
|
||||
submission_created: 'bg-orange-500',
|
||||
submission_reviewed: 'bg-green-500',
|
||||
comment_added: 'bg-gray-500'
|
||||
}
|
||||
return colorMap[type] || 'bg-gray-500'
|
||||
}
|
||||
|
||||
function getActivityTitle(type: ActivityType): string {
|
||||
const titleMap: Record<string, string> = {
|
||||
task_created: 'Task Created',
|
||||
task_updated: 'Task Updated',
|
||||
task_assigned: 'Task Assigned',
|
||||
task_status_changed: 'Status Changed',
|
||||
submission_created: 'Work Submitted',
|
||||
submission_reviewed: 'Submission Reviewed',
|
||||
comment_added: 'Comment Added'
|
||||
}
|
||||
return titleMap[type] || 'Activity'
|
||||
}
|
||||
|
||||
function formatTime(timestamp: string): string {
|
||||
const date = new Date(timestamp)
|
||||
const now = new Date()
|
||||
const diffMs = now.getTime() - date.getTime()
|
||||
const diffMins = Math.floor(diffMs / 60000)
|
||||
const diffHours = Math.floor(diffMs / 3600000)
|
||||
const diffDays = Math.floor(diffMs / 86400000)
|
||||
|
||||
if (diffMins < 1) return 'Just now'
|
||||
if (diffMins < 60) return `${diffMins}m ago`
|
||||
if (diffHours < 24) return `${diffHours}h ago`
|
||||
if (diffDays < 7) return `${diffDays}d ago`
|
||||
|
||||
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString()
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user