From 841e786fdd432bd709c0c04a405361daaa6700c2 Mon Sep 17 00:00:00 2001 From: indigo Date: Sat, 18 Jul 2026 02:17:06 +0800 Subject: [PATCH] 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. --- .../src/components/shot/ShotDetailPanel.vue | 97 +++++++++++-------- 1 file changed, 58 insertions(+), 39 deletions(-) diff --git a/frontend/src/components/shot/ShotDetailPanel.vue b/frontend/src/components/shot/ShotDetailPanel.vue index 93c5583..950d164 100644 --- a/frontend/src/components/shot/ShotDetailPanel.vue +++ b/frontend/src/components/shot/ShotDetailPanel.vue @@ -185,13 +185,15 @@ @click="$emit('select-task', task, 'infos')" >
{{ formatTaskType(task.task_type) }}
-
- {{ task.assigned_user_name || 'Unassigned' }} +
+ + + {{ getTaskAssigneeInitials(task) }} + + {{ task.assigned_user_name || 'Unassigned' }}
- - {{ formatTaskStatus(task.status.toString()) }} - +
@@ -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() const emit = defineEmits() const authStore = useAuthStore() +const taskStatusesStore = useTaskStatusesStore() +const { getAvatarUrl } = useAvatarUrl() // Reactive state const shot = ref(null) @@ -382,6 +394,7 @@ const tasks = ref([]) const isLoading = ref(false) const error = ref(null) const isCreatingTask = ref(false) +const projectMembers = ref([]) // 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', {