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')" @click="$emit('select-task', task, 'infos')"
> >
<div class="text-sm font-medium">{{ formatTaskType(task.task_type) }}</div> <div class="text-sm font-medium">{{ formatTaskType(task.task_type) }}</div>
<div class="text-sm text-muted-foreground"> <div class="flex items-center gap-2 text-sm text-muted-foreground min-w-0">
{{ task.assigned_user_name || 'Unassigned' }} <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>
<div> <div>
<Badge :variant="getTaskStatusVariant(task.status)" class="text-xs"> <TaskStatusBadge :status="getTaskStatusObject(task)" compact />
{{ formatTaskStatus(task.status.toString()) }}
</Badge>
</div> </div>
</div> </div>
</div> </div>
@@ -339,19 +341,27 @@ import { Badge } from '@/components/ui/badge'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import DetailPanelLoading from '@/components/shared/DetailPanelLoading.vue' import DetailPanelLoading from '@/components/shared/DetailPanelLoading.vue'
import DetailPanelError from '@/components/shared/DetailPanelError.vue' import DetailPanelError from '@/components/shared/DetailPanelError.vue'
import DetailPanelHeader from '@/components/shared/DetailPanelHeader.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 { taskService } from '@/services/task'
import { projectService, type ProjectMember } from '@/services/project'
import { useAuthStore } from '@/stores/auth' 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 // Use TaskStatusInfo from shot service instead of local Task interface
interface Task extends TaskStatusInfo { interface Task extends TaskStatusInfo {
id: number id: number
name?: string name?: string
assigned_user_name?: string assigned_user_name?: string
assigned_user_avatar_url?: string | null
assigned_user_first_name?: string
assigned_user_last_name?: string
deadline?: string deadline?: string
} }
@@ -375,6 +385,8 @@ const props = defineProps<Props>()
const emit = defineEmits<Emits>() const emit = defineEmits<Emits>()
const authStore = useAuthStore() const authStore = useAuthStore()
const taskStatusesStore = useTaskStatusesStore()
const { getAvatarUrl } = useAvatarUrl()
// Reactive state // Reactive state
const shot = ref<Shot | null>(null) const shot = ref<Shot | null>(null)
@@ -382,6 +394,7 @@ const tasks = ref<Task[]>([])
const isLoading = ref(false) const isLoading = ref(false)
const error = ref<string | null>(null) const error = ref<string | null>(null)
const isCreatingTask = ref(false) const isCreatingTask = ref(false)
const projectMembers = ref<ProjectMember[]>([])
// Computed properties // Computed properties
const frameCount = computed(() => { const frameCount = computed(() => {
@@ -450,6 +463,10 @@ const loadShotDetails = async () => {
isLoading.value = true isLoading.value = true
error.value = null error.value = null
shot.value = props.initialShot ?? await shotService.getShot(props.shotId) shot.value = props.initialShot ?? await shotService.getShot(props.shotId)
await Promise.all([
taskStatusesStore.fetchProjectStatuses(props.projectId),
loadProjectMembers()
])
loadTasks() // No longer async - uses embedded data loadTasks() // No longer async - uses embedded data
} catch (err) { } catch (err) {
error.value = err instanceof Error ? err.message : 'Failed to load shot details' 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 = () => { const loadTasks = () => {
// Use task_details already embedded in shot data - no API call needed! // Use task_details already embedded in shot data - no API call needed!
if (shot.value?.task_details) { if (shot.value?.task_details) {
tasks.value = shot.value.task_details.map(taskInfo => ({ tasks.value = shot.value.task_details.map(taskInfo => {
id: taskInfo.task_id || 0, const assignedUser = projectMembers.value.find(member => member.user_id === taskInfo.assigned_user_id)
task_type: taskInfo.task_type, return {
status: taskInfo.status, id: taskInfo.task_id || 0,
assigned_user_id: taskInfo.assigned_user_id, task_type: taskInfo.task_type,
// Add placeholder values for display compatibility status: taskInfo.status,
name: taskInfo.task_type, // Use task_type as name for display assigned_user_id: taskInfo.assigned_user_id,
assigned_user_name: undefined // Will be resolved if needed // 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 { } else {
tasks.value = [] 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) => { const handleAddTask = async (taskType: string) => {
isCreatingTask.value = true isCreatingTask.value = true
try { try {
@@ -496,18 +533,18 @@ const formatStatus = (status: ShotStatus) => {
).join(' ') ).join(' ')
} }
const formatTaskStatus = (status: string) => {
return status.split('_').map(word =>
word.charAt(0).toUpperCase() + word.slice(1)
).join(' ')
}
const formatTaskType = (taskType: string) => { const formatTaskType = (taskType: string) => {
return taskType.split('_').map(word => return taskType.split('_').map(word =>
word.charAt(0).toUpperCase() + word.slice(1) word.charAt(0).toUpperCase() + word.slice(1)
).join(' ') ).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) => { const getStatusVariant = (status: ShotStatus) => {
switch (status) { switch (status) {
case ShotStatus.NOT_STARTED: 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 formatDate = (dateString: string) => {
const date = new Date(dateString) const date = new Date(dateString)
return date.toLocaleDateString('en-US', { return date.toLocaleDateString('en-US', {