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:
@@ -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 => {
|
||||||
|
const assignedUser = projectMembers.value.find(member => member.user_id === taskInfo.assigned_user_id)
|
||||||
|
return {
|
||||||
id: taskInfo.task_id || 0,
|
id: taskInfo.task_id || 0,
|
||||||
task_type: taskInfo.task_type,
|
task_type: taskInfo.task_type,
|
||||||
status: taskInfo.status,
|
status: taskInfo.status,
|
||||||
assigned_user_id: taskInfo.assigned_user_id,
|
assigned_user_id: taskInfo.assigned_user_id,
|
||||||
// Add placeholder values for display compatibility
|
// Add placeholder values for display compatibility
|
||||||
name: taskInfo.task_type, // Use task_type as name for display
|
name: taskInfo.task_type, // Use task_type as name for display
|
||||||
assigned_user_name: undefined // Will be resolved if needed
|
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', {
|
||||||
|
|||||||
Reference in New Issue
Block a user