Modify Shot Data Table Functions

This commit is contained in:
2026-03-05 22:12:02 +08:00
parent c8c4c99a6e
commit 1f229bff6c
26 changed files with 212 additions and 186 deletions
@@ -39,24 +39,50 @@
<Button
variant="ghost"
size="sm"
class="h-6 w-6 p-0 hover:bg-accent relative z-10"
class="h-6 w-6 p-0 hover:bg-accent relative"
:disabled="isUpdating"
@click="ensureMembersLoaded"
@click.stop="ensureMembersLoaded"
>
<Avatar class="h-4 w-4" v-if="assignedUser">
<AvatarImage :src="getAvatarUrl(assignedUser.user_id)" />
<AvatarImage :src="getAvatarUrl(assignedUser?.user_avatar_url, assignedUser?.user_first_name, assignedUser?.user_last_name)" />
<AvatarFallback class="text-[8px]">{{ getUserInitials(assignedUser) }}</AvatarFallback>
</Avatar>
<User class="h-3 w-3" v-else />
</Button>
</PopoverTrigger>
<PopoverContent class="w-64 p-2 z-50" align="start" side="bottom" :side-offset="4">
<PopoverContent class="w-64 p-2" align="start" side="bottom" :side-offset="4">
<div class="space-y-2">
<div class="px-2 py-1.5 text-sm font-semibold">Assign Task</div>
<!-- Debug info -->
<div class="px-2 py-1 text-xs text-muted-foreground">
Project ID: {{ projectId }}, Members: {{ projectMembers.length }}
<!-- Current Assignment Display with X button -->
<div v-if="assignedUser" class="px-2 py-2 bg-muted rounded-md flex items-center gap-2">
<Avatar class="h-8 w-8">
<AvatarImage :src="getAvatarUrl(assignedUser?.user_avatar_url, assignedUser?.user_first_name, assignedUser?.user_last_name)" />
<AvatarFallback class="text-[8px]">{{ getUserInitials(assignedUser) }}</AvatarFallback>
</Avatar>
<div class="flex flex-col flex-1 min-w-0">
<span class="text-xs font-medium truncate">{{ assignedUser.user_first_name }} {{ assignedUser.user_last_name }}</span>
<span class="text-[10px] text-muted-foreground">Current</span>
</div>
<Button
variant="ghost"
size="sm"
class="h-6 w-6 p-0 hover:bg-destructive hover:text-destructive-foreground"
@click.stop="handleAssignUser(null)"
:disabled="isAssigning"
>
<X class="h-3 w-3" />
</Button>
</div>
<!-- Search Input -->
<div class="relative">
<Search class="absolute left-2 top-1/2 transform -translate-y-1/2 h-3 w-3 text-muted-foreground" />
<Input
v-model="searchQuery"
placeholder="Search members..."
class="pl-7 h-8 text-xs"
/>
</div>
<!-- Loading state -->
@@ -80,37 +106,30 @@
<!-- Content when members are loaded -->
<template v-else>
<!-- Unassign option -->
<Button
variant="ghost"
size="sm"
class="w-full justify-start"
@click="handleAssignUser(null)"
:disabled="isAssigning"
>
<UserX class="h-4 w-4 mr-2" />
Unassign
</Button>
<!-- Project members list -->
<div class="max-h-48 overflow-y-auto">
<div class="max-h-64 overflow-y-auto">
<div v-if="filteredProjectMembers.length === 0" class="py-2 text-xs text-muted-foreground text-center">
No matching members found
</div>
<Button
v-for="member in projectMembers"
v-else
v-for="member in filteredProjectMembers"
:key="member.user_id"
variant="ghost"
size="sm"
class="w-full justify-start"
class="w-full justify-start h-10"
@click="handleAssignUser(member.user_id)"
:disabled="isAssigning"
>
<Avatar class="h-4 w-4 mr-2">
<AvatarImage :src="getAvatarUrl(member.user_id)" />
<Avatar class="h-8 w-8 mr-2">
<AvatarImage :src="getAvatarUrl(member.user_avatar_url, member.user_first_name, member.user_last_name)" />
<AvatarFallback class="text-[8px]">{{ getUserInitials(member) }}</AvatarFallback>
</Avatar>
<div class="flex flex-col items-start">
<span class="text-sm">{{ member.user_first_name }} {{ member.user_last_name }}</span>
<span class="text-xs text-muted-foreground" v-if="member.department_role">{{ formatDepartmentRole(member.department_role) }}</span>
<div class="flex flex-col items-start flex-1 min-w-0">
<span class="text-xs truncate">{{ member.user_first_name }} {{ member.user_last_name }}</span>
<span class="text-[10px] text-muted-foreground" v-if="member.department_role">{{ formatDepartmentRole(member.department_role) }}</span>
</div>
<Check v-if="assignedUserId === member.user_id" class="h-4 w-4 text-green-500 ml-1" />
</Button>
</div>
</template>
@@ -144,8 +163,9 @@ import {
PopoverTrigger,
} from '@/components/ui/popover'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { User, UserX } from 'lucide-vue-next'
import { User, UserX, Search, Check, X } from 'lucide-vue-next'
import TaskStatusBadge from '@/components/task/TaskStatusBadge.vue'
import { TaskStatus } from '@/services/shot'
import { taskService } from '@/services/task'
@@ -183,6 +203,20 @@ const isUpdating = ref(false)
const isAssigning = ref(false)
const isLoadingMembers = ref(false)
const projectMembers = ref<ProjectMember[]>([])
const searchQuery = ref('')
// Filtered project members based on search query
const filteredProjectMembers = computed(() => {
if (!searchQuery.value.trim()) {
return projectMembers.value
}
const query = searchQuery.value.toLowerCase().trim()
return projectMembers.value.filter(member => {
const fullName = `${member.user_first_name || ''} ${member.user_last_name || ''}`.toLowerCase()
const departmentRole = member.department_role?.toLowerCase() || ''
return fullName.includes(query) || departmentRole.includes(query)
})
})
// Get loading state from store
const isLoadingStatuses = computed(() => taskStatusesStore.isLoading(props.projectId))
@@ -257,10 +291,9 @@ const getUserInitials = (member: ProjectMember): string => {
return (first + last).toUpperCase()
}
// Get avatar URL
const getAvatarUrl = (userId: number): string => {
return `https://api.dicebear.com/7.x/initials/svg?seed=${userId}`
}
import { useAvatarUrl } from '@/composables/useAvatarUrl'
const { getAvatarUrl, getInitialsAvatarUrl } = useAvatarUrl()
// Fetch custom statuses for the project using store
const fetchStatuses = async () => {
@@ -353,7 +386,10 @@ const handleAssignUser = async (userId: number | null) => {
emit('assignment-updated', props.shotId, props.taskType, userId)
}
// Note: Popover will close naturally when clicking outside or on assignment
// Close popover by simulating click outside after assignment
setTimeout(() => {
document.querySelector('[data-state="open"]')?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
}, 100)
} catch (error) {
console.error('Failed to assign task:', error)
} finally {