Fix task assignee resolution and align task status styling in ShotDetailPanel

The Tasks list embedded assigned_user_name: undefined with a "will be
resolved if needed" comment that was never followed up on, so every task
showed "Unassigned" regardless of actual assignment. Resolve it from the
project members list, matching how the shot table's own assignment control
does it, and show an avatar next to the name. Also swap the hardcoded
status Badge/variant switch for the same TaskStatusBadge + task-statuses
store the shot table uses, so status colors and labels (including custom
per-project statuses) always match between the table and the panel.
This commit is contained in:
2026-07-18 02:17:06 +08:00
parent 26807984ee
commit 841e786fdd
@@ -185,13 +185,15 @@
@click="$emit('select-task', task, 'infos')"
>
<div class="text-sm font-medium">{{ formatTaskType(task.task_type) }}</div>
<div class="text-sm text-muted-foreground">
{{ task.assigned_user_name || 'Unassigned' }}
<div class="flex items-center gap-2 text-sm text-muted-foreground min-w-0">
<Avatar class="h-5 w-5 flex-shrink-0" v-if="task.assigned_user_name">
<AvatarImage :src="getAvatarUrl(task.assigned_user_avatar_url, task.assigned_user_first_name, task.assigned_user_last_name)" />
<AvatarFallback class="text-[9px]">{{ getTaskAssigneeInitials(task) }}</AvatarFallback>
</Avatar>
<span class="truncate">{{ task.assigned_user_name || 'Unassigned' }}</span>
</div>
<div>
<Badge :variant="getTaskStatusVariant(task.status)" class="text-xs">
{{ formatTaskStatus(task.status.toString()) }}
</Badge>
<TaskStatusBadge :status="getTaskStatusObject(task)" compact />
</div>
</div>
</div>
@@ -339,19 +341,27 @@ import { Badge } from '@/components/ui/badge'
import { Label } from '@/components/ui/label'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import DetailPanelLoading from '@/components/shared/DetailPanelLoading.vue'
import DetailPanelError from '@/components/shared/DetailPanelError.vue'
import DetailPanelHeader from '@/components/shared/DetailPanelHeader.vue'
import TaskStatusBadge from '@/components/task/TaskStatusBadge.vue'
import { shotService, ShotStatus, type Shot, type TaskStatusInfo, TaskStatus } from '@/services/shot'
import { shotService, ShotStatus, type Shot, type TaskStatusInfo } from '@/services/shot'
import { taskService } from '@/services/task'
import { projectService, type ProjectMember } from '@/services/project'
import { useAuthStore } from '@/stores/auth'
import { useTaskStatusesStore } from '@/stores/taskStatuses'
import { useAvatarUrl } from '@/composables/useAvatarUrl'
// Use TaskStatusInfo from shot service instead of local Task interface
interface Task extends TaskStatusInfo {
id: number
name?: string
assigned_user_name?: string
assigned_user_avatar_url?: string | null
assigned_user_first_name?: string
assigned_user_last_name?: string
deadline?: string
}
@@ -375,6 +385,8 @@ const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const authStore = useAuthStore()
const taskStatusesStore = useTaskStatusesStore()
const { getAvatarUrl } = useAvatarUrl()
// Reactive state
const shot = ref<Shot | null>(null)
@@ -382,6 +394,7 @@ const tasks = ref<Task[]>([])
const isLoading = ref(false)
const error = ref<string | null>(null)
const isCreatingTask = ref(false)
const projectMembers = ref<ProjectMember[]>([])
// Computed properties
const frameCount = computed(() => {
@@ -450,6 +463,10 @@ const loadShotDetails = async () => {
isLoading.value = true
error.value = null
shot.value = props.initialShot ?? await shotService.getShot(props.shotId)
await Promise.all([
taskStatusesStore.fetchProjectStatuses(props.projectId),
loadProjectMembers()
])
loadTasks() // No longer async - uses embedded data
} catch (err) {
error.value = err instanceof Error ? err.message : 'Failed to load shot details'
@@ -459,23 +476,43 @@ const loadShotDetails = async () => {
}
}
const loadProjectMembers = async () => {
try {
projectMembers.value = await projectService.getProjectMembers(props.projectId)
} catch (err) {
console.error('Failed to load project members:', err)
}
}
const loadTasks = () => {
// Use task_details already embedded in shot data - no API call needed!
if (shot.value?.task_details) {
tasks.value = shot.value.task_details.map(taskInfo => ({
id: taskInfo.task_id || 0,
task_type: taskInfo.task_type,
status: taskInfo.status,
assigned_user_id: taskInfo.assigned_user_id,
// Add placeholder values for display compatibility
name: taskInfo.task_type, // Use task_type as name for display
assigned_user_name: undefined // Will be resolved if needed
}))
tasks.value = shot.value.task_details.map(taskInfo => {
const assignedUser = projectMembers.value.find(member => member.user_id === taskInfo.assigned_user_id)
return {
id: taskInfo.task_id || 0,
task_type: taskInfo.task_type,
status: taskInfo.status,
assigned_user_id: taskInfo.assigned_user_id,
// Add placeholder values for display compatibility
name: taskInfo.task_type, // Use task_type as name for display
assigned_user_name: assignedUser ? `${assignedUser.user_first_name} ${assignedUser.user_last_name}` : undefined,
assigned_user_avatar_url: assignedUser?.user_avatar_url,
assigned_user_first_name: assignedUser?.user_first_name,
assigned_user_last_name: assignedUser?.user_last_name
}
})
} else {
tasks.value = []
}
}
// Resolve the project's actual status object (name + color) for a task, matching the shot table
const getTaskStatusObject = (task: Task) => {
const statusId = task.status.toString()
return taskStatusesStore.getStatusById(props.projectId, statusId) || statusId
}
const handleAddTask = async (taskType: string) => {
isCreatingTask.value = true
try {
@@ -496,18 +533,18 @@ const formatStatus = (status: ShotStatus) => {
).join(' ')
}
const formatTaskStatus = (status: string) => {
return status.split('_').map(word =>
word.charAt(0).toUpperCase() + word.slice(1)
).join(' ')
}
const formatTaskType = (taskType: string) => {
return taskType.split('_').map(word =>
word.charAt(0).toUpperCase() + word.slice(1)
).join(' ')
}
const getTaskAssigneeInitials = (task: Task) => {
const first = task.assigned_user_first_name?.charAt(0) || ''
const last = task.assigned_user_last_name?.charAt(0) || ''
return (first + last).toUpperCase()
}
const getStatusVariant = (status: ShotStatus) => {
switch (status) {
case ShotStatus.NOT_STARTED:
@@ -542,24 +579,6 @@ const getStatusColor = (status: ShotStatus) => {
}
}
const getTaskStatusVariant = (status: string | TaskStatus) => {
const statusStr = status.toString()
switch (statusStr) {
case 'not_started':
return 'secondary'
case 'in_progress':
return 'default'
case 'submitted':
return 'outline'
case 'approved':
return 'default'
case 'retake':
return 'destructive'
default:
return 'secondary'
}
}
const formatDate = (dateString: string) => {
const date = new Date(dateString)
return date.toLocaleDateString('en-US', {