Init Repo
This commit is contained in:
@@ -0,0 +1,377 @@
|
||||
<template>
|
||||
<div class="relative flex items-center gap-1" v-memo="[currentStatusId, isUpdating, isLoadingStatuses, assignedUserId]" data-testid="editable-task-status">
|
||||
<!-- Status Selector -->
|
||||
<Select
|
||||
:model-value="currentStatusId"
|
||||
@update:model-value="handleStatusChange"
|
||||
:disabled="isUpdating || isLoadingStatuses"
|
||||
>
|
||||
<SelectTrigger class="h-6 w-[130px] font-semibold text-xs"
|
||||
:style="{ backgroundColor: currentStatusObject.color }"
|
||||
>
|
||||
<SelectValue>
|
||||
<!-- <TaskStatusBadge :status="currentStatusObject"/> -->
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem
|
||||
v-for="statusOption in allStatusOptions"
|
||||
:key="statusOption.id"
|
||||
:value="statusOption.id"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- Color indicator -->
|
||||
<!-- <div
|
||||
v-if="statusOption.color"
|
||||
class="w-3 h-3 rounded-full border border-border"
|
||||
:style="{ backgroundColor: statusOption.color }"
|
||||
/> -->
|
||||
<TaskStatusBadge :status="statusOption" compact />
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<!-- User Assignment Button -->
|
||||
<div @click.stop>
|
||||
<Popover>
|
||||
<PopoverTrigger as-child>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-6 w-6 p-0 hover:bg-accent relative z-10"
|
||||
:disabled="isUpdating"
|
||||
@click="ensureMembersLoaded"
|
||||
>
|
||||
<Avatar class="h-4 w-4" v-if="assignedUser">
|
||||
<AvatarImage :src="getAvatarUrl(assignedUser.user_id)" />
|
||||
<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">
|
||||
<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 }}
|
||||
</div>
|
||||
|
||||
<!-- Loading state -->
|
||||
<div v-if="isLoadingMembers" class="flex items-center justify-center py-4">
|
||||
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-primary"></div>
|
||||
<span class="ml-2 text-sm">Loading members...</span>
|
||||
</div>
|
||||
|
||||
<!-- Error state -->
|
||||
<div v-else-if="projectMembers.length === 0" class="px-2 py-4 text-sm text-muted-foreground text-center">
|
||||
No project members found
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="mt-2"
|
||||
@click="loadProjectMembers"
|
||||
>
|
||||
Retry
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- 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">
|
||||
<Button
|
||||
v-for="member in projectMembers"
|
||||
:key="member.user_id"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="w-full justify-start"
|
||||
@click="handleAssignUser(member.user_id)"
|
||||
:disabled="isAssigning"
|
||||
>
|
||||
<Avatar class="h-4 w-4 mr-2">
|
||||
<AvatarImage :src="getAvatarUrl(member.user_id)" />
|
||||
<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>
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<!-- Loading indicator -->
|
||||
<div
|
||||
v-if="isUpdating || isLoadingStatuses"
|
||||
class="absolute inset-0 bg-background/50 flex items-center justify-center rounded"
|
||||
>
|
||||
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-primary"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { User, UserX } from 'lucide-vue-next'
|
||||
import TaskStatusBadge from '@/components/task/TaskStatusBadge.vue'
|
||||
import { TaskStatus } from '@/services/shot'
|
||||
import { taskService } from '@/services/task'
|
||||
import { projectService, type ProjectMember } from '@/services/project'
|
||||
import { useTaskStatusesStore } from '@/stores/taskStatuses'
|
||||
|
||||
interface StatusOption {
|
||||
id: string
|
||||
name: string
|
||||
color?: string
|
||||
is_system?: boolean
|
||||
}
|
||||
|
||||
interface Props {
|
||||
shotId: number
|
||||
taskType: string
|
||||
status: TaskStatus | string
|
||||
taskId?: number | null
|
||||
projectId: number
|
||||
assignedUserId?: number | null
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'status-updated', shotId: number, taskType: string, newStatus: string): void
|
||||
(e: 'assignment-updated', shotId: number, taskType: string, userId: number | null): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
// Use the shared task statuses store instead of direct API calls
|
||||
const taskStatusesStore = useTaskStatusesStore()
|
||||
|
||||
const isUpdating = ref(false)
|
||||
const isAssigning = ref(false)
|
||||
const isLoadingMembers = ref(false)
|
||||
const projectMembers = ref<ProjectMember[]>([])
|
||||
|
||||
// Get loading state from store
|
||||
const isLoadingStatuses = computed(() => taskStatusesStore.isLoading(props.projectId))
|
||||
|
||||
// Get all status options from store
|
||||
const allStatusOptions = computed(() => taskStatusesStore.getAllStatusOptions(props.projectId))
|
||||
|
||||
// Get current status ID (handle both TaskStatus enum and custom status strings)
|
||||
const currentStatusId = computed(() => {
|
||||
if (typeof props.status === 'string') {
|
||||
return props.status
|
||||
}
|
||||
return props.status as string
|
||||
})
|
||||
|
||||
// Get current status object for display using store
|
||||
const currentStatusObject = computed((): StatusOption => {
|
||||
const statusFromStore = taskStatusesStore.getStatusById(props.projectId, currentStatusId.value)
|
||||
if (statusFromStore) {
|
||||
return {
|
||||
id: statusFromStore.id,
|
||||
name: statusFromStore.name,
|
||||
color: statusFromStore.color,
|
||||
is_system: 'is_system' in statusFromStore ? statusFromStore.is_system : false
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to current status as-is
|
||||
return {
|
||||
id: currentStatusId.value,
|
||||
name: formatStatusName(currentStatusId.value)
|
||||
}
|
||||
})
|
||||
|
||||
// Get assigned user info
|
||||
const assignedUserId = computed(() => props.assignedUserId)
|
||||
const assignedUser = computed(() => {
|
||||
if (!assignedUserId.value) return null
|
||||
return projectMembers.value.find(member => member.user_id === assignedUserId.value) || null
|
||||
})
|
||||
|
||||
// Format status name for display
|
||||
const formatStatusName = (status: string): string => {
|
||||
switch (status) {
|
||||
case 'not_started':
|
||||
return 'Not Started'
|
||||
case 'in_progress':
|
||||
return 'In Progress'
|
||||
case 'submitted':
|
||||
return 'Submitted'
|
||||
case 'approved':
|
||||
return 'Approved'
|
||||
case 'retake':
|
||||
return 'Retake'
|
||||
default:
|
||||
// Convert snake_case to Title Case
|
||||
return status.split('_').map(word =>
|
||||
word.charAt(0).toUpperCase() + word.slice(1)
|
||||
).join(' ')
|
||||
}
|
||||
}
|
||||
|
||||
// Format department role for display
|
||||
const formatDepartmentRole = (role: string): string => {
|
||||
return role.charAt(0).toUpperCase() + role.slice(1)
|
||||
}
|
||||
|
||||
// Get user initials
|
||||
const getUserInitials = (member: ProjectMember): string => {
|
||||
const first = member.user_first_name?.charAt(0) || ''
|
||||
const last = member.user_last_name?.charAt(0) || ''
|
||||
return (first + last).toUpperCase()
|
||||
}
|
||||
|
||||
// Get avatar URL
|
||||
const getAvatarUrl = (userId: number): string => {
|
||||
return `https://api.dicebear.com/7.x/initials/svg?seed=${userId}`
|
||||
}
|
||||
|
||||
// Fetch custom statuses for the project using store
|
||||
const fetchStatuses = async () => {
|
||||
if (!props.projectId) return
|
||||
|
||||
try {
|
||||
await taskStatusesStore.fetchProjectStatuses(props.projectId)
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch task statuses:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Load project members
|
||||
const loadProjectMembers = async () => {
|
||||
if (projectMembers.value.length > 0) return // Already loaded
|
||||
|
||||
isLoadingMembers.value = true
|
||||
try {
|
||||
console.log('Loading project members for project:', props.projectId)
|
||||
projectMembers.value = await projectService.getProjectMembers(props.projectId)
|
||||
console.log('Loaded project members:', projectMembers.value)
|
||||
} catch (error) {
|
||||
console.error('Failed to load project members:', error)
|
||||
} finally {
|
||||
isLoadingMembers.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure members are loaded when popover is about to open
|
||||
const ensureMembersLoaded = () => {
|
||||
console.log('Ensuring project members are loaded')
|
||||
if (projectMembers.value.length === 0) {
|
||||
console.log('Loading project members on button click')
|
||||
loadProjectMembers()
|
||||
}
|
||||
}
|
||||
|
||||
const handleStatusChange = async (newStatusId: any) => {
|
||||
if (!newStatusId || newStatusId === currentStatusId.value) return
|
||||
|
||||
const statusId = newStatusId as string
|
||||
|
||||
isUpdating.value = true
|
||||
|
||||
try {
|
||||
let taskId = props.taskId
|
||||
|
||||
// If no task exists, create one first
|
||||
if (!taskId) {
|
||||
const newTask = await taskService.createShotTask(props.shotId, props.taskType)
|
||||
taskId = newTask.task_id
|
||||
}
|
||||
|
||||
// Update the task status
|
||||
if (taskId) {
|
||||
await taskService.updateTaskStatus(taskId, statusId as TaskStatus)
|
||||
emit('status-updated', props.shotId, props.taskType, statusId)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to update task status:', error)
|
||||
// Revert the status change by emitting the original status
|
||||
// This will cause the parent component to refresh the data
|
||||
emit('status-updated', props.shotId, props.taskType, currentStatusId.value)
|
||||
} finally {
|
||||
isUpdating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleAssignUser = async (userId: number | null) => {
|
||||
isAssigning.value = true
|
||||
|
||||
try {
|
||||
let taskId = props.taskId
|
||||
|
||||
// If no task exists, create one first
|
||||
if (!taskId) {
|
||||
const newTask = await taskService.createShotTask(props.shotId, props.taskType)
|
||||
taskId = newTask.task_id
|
||||
}
|
||||
|
||||
// Assign or unassign the task
|
||||
if (taskId) {
|
||||
if (userId) {
|
||||
// Use the assignment endpoint for assigning to a user
|
||||
await taskService.assignTask(taskId, userId)
|
||||
} else {
|
||||
// Use the update endpoint for unassignment (set assigned_user_id to 0)
|
||||
await taskService.updateTask(taskId, { assigned_user_id: 0 })
|
||||
}
|
||||
emit('assignment-updated', props.shotId, props.taskType, userId)
|
||||
}
|
||||
|
||||
// Note: Popover will close naturally when clicking outside or on assignment
|
||||
} catch (error) {
|
||||
console.error('Failed to assign task:', error)
|
||||
} finally {
|
||||
isAssigning.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch statuses on mount
|
||||
onMounted(() => {
|
||||
fetchStatuses()
|
||||
// Preload project members to ensure they're available when needed
|
||||
loadProjectMembers()
|
||||
})
|
||||
|
||||
// Refetch statuses when projectId changes
|
||||
watch(() => props.projectId, () => {
|
||||
fetchStatuses()
|
||||
// Clear project members when project changes
|
||||
projectMembers.value = []
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user