@@ -182,17 +285,19 @@
\ No newline at end of file
diff --git a/frontend/src/components/asset/columns.ts b/frontend/src/components/asset/columns.ts
index ca5cc78..7851041 100644
--- a/frontend/src/components/asset/columns.ts
+++ b/frontend/src/components/asset/columns.ts
@@ -55,6 +55,7 @@ export interface AssetColumnMeta {
onDelete: (asset: Asset) => void
onViewTasks: (asset: Asset) => void
onTaskStatusUpdated: (assetId: number, taskType: string, newStatus: TaskStatus) => void
+ onTaskAssignmentUpdated?: (assetId: number, taskType: string, userId: number | null) => void
onBulkTaskStatusChange?: (taskType: string, status: TaskStatus) => void
getSelectedCount?: () => number
getAllStatusOptions?: () => Array<{ id: string; name: string; color?: string; is_system?: boolean }>
@@ -225,8 +226,10 @@ export const createAssetColumns = (
cell: ({ row }) => {
const asset = row.original
const status = asset.task_status?.[taskType] || TaskStatus.NOT_STARTED
- const taskId = asset.task_details?.find(t => t.task_type === taskType)?.task_id
-
+ const taskDetail = asset.task_details?.find(t => t.task_type === taskType)
+ const taskId = taskDetail?.task_id
+ const assignedUserId = taskDetail?.assigned_user_id || null
+
return h(EditableTaskStatus, {
key: `${asset.id}-${taskType}`, // Add stable key to prevent unnecessary re-renders
assetId: asset.id,
@@ -234,9 +237,13 @@ export const createAssetColumns = (
status,
taskId,
projectId: meta.projectId,
+ assignedUserId,
onStatusUpdated: (assetId: number, taskType: string, newStatus: TaskStatus) => {
meta.onTaskStatusUpdated(assetId, taskType, newStatus)
},
+ onAssignmentUpdated: (assetId: number, taskType: string, userId: number | null) => {
+ meta.onTaskAssignmentUpdated?.(assetId, taskType, userId)
+ },
})
},
enableSorting: true,
diff --git a/frontend/src/components/project/ProjectMemberManagement.vue b/frontend/src/components/project/ProjectMemberManagement.vue
index df9f5e0..25b6ad9 100644
--- a/frontend/src/components/project/ProjectMemberManagement.vue
+++ b/frontend/src/components/project/ProjectMemberManagement.vue
@@ -41,14 +41,7 @@
>
-
-
+
{{ getUserInitials(member) }}
@@ -65,21 +58,21 @@
Department:
- updateMemberRole(member.id, (event.target as HTMLSelectElement).value === 'none' ? null : (event.target as HTMLSelectElement).value)"
+ updateMemberRole(member, value === 'none' ? null : (value as string))"
:disabled="isUpdatingMember === member.id"
- class="flex h-8 w-32 rounded-md border border-input bg-background px-2 py-1 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
>
- None
- Layout
- Animation
- Lighting
- Composite
- Modeling
- Rigging
- Surfacing
-
+
+
+
+
+ None
+
+ {{ role.label }}
+
+
+
@@ -89,7 +82,7 @@
-
+
@@ -118,119 +111,84 @@
-
-
-
-
-
-
-
-
-
Add Team Member
-
+
+
+
+
+ Add Team Member
+
Add a user to this project and assign their department role.
-
-
-
+
+
+
-
- User
-
- Select a user
-
- {{ user.first_name }} {{ user.last_name }} ({{ user.email }})
-
-
+ User
+
+
+
+
+
+
+ {{ user.first_name }} {{ user.last_name }} ({{ user.email }})
+
+
+
-
- Department Role (Optional)
-
- None
- Layout
- Animation
- Lighting
- Composite
- Modeling
- Rigging
- Surfacing
-
+ Department Role (Optional)
+
+
+
+
+
+ None
+
+ {{ role.label }}
+
+
+
-
-
+
+
Cancel
-
+
Add Member
-
-
-
+
+
+
-
-
-
-
-
-
-
-
Remove Team Member
-
+
+
+
+ Remove Team Member
+
Are you sure you want to remove "{{ memberToRemove?.user_first_name }} {{ memberToRemove?.user_last_name }}" from this project?
This action cannot be undone.
-
-
-
-
-
- Cancel
-
-
+
+
+
+ Cancel
+
Remove Member
-
-
-
-
+
+
+
+
@@ -242,14 +200,39 @@ import {
import { Button } from '@/components/ui/button'
import { Label } from '@/components/ui/label'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
-
-
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select'
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog'
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+} from '@/components/ui/alert-dialog'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
+import { useAvatarUrl } from '@/composables/useAvatarUrl'
+import { useToast } from '@/components/ui/toast/use-toast'
import { projectService, type ProjectMember } from '@/services/project'
import { userService } from '@/services/user'
import type { User } from '@/types/auth'
@@ -259,10 +242,26 @@ interface Props {
}
const props = defineProps
()
+const emit = defineEmits<{
+ close: []
+}>()
+
+const { toast } = useToast()
+const { getAvatarUrl } = useAvatarUrl()
+
+const departmentRoles = [
+ { value: 'layout', label: 'Layout' },
+ { value: 'animation', label: 'Animation' },
+ { value: 'lighting', label: 'Lighting' },
+ { value: 'composite', label: 'Composite' },
+ { value: 'modeling', label: 'Modeling' },
+ { value: 'rigging', label: 'Rigging' },
+ { value: 'surfacing', label: 'Surfacing' },
+]
// State
const members = ref([])
-const availableUsers = ref([])
+const allUsers = ref([])
const isLoading = ref(false)
const error = ref(null)
const isUpdatingMember = ref(null)
@@ -273,7 +272,13 @@ const memberToRemove = ref(null)
const newMember = ref({
userId: '',
- departmentRole: ''
+ departmentRole: 'none'
+})
+
+// Computed
+const availableUsers = computed(() => {
+ const memberUserIds = new Set(members.value.map(m => m.user_id))
+ return allUsers.value.filter(user => user.is_approved && !memberUserIds.has(user.id))
})
// Methods
@@ -284,40 +289,50 @@ const loadMembers = async () => {
members.value = await projectService.getProjectMembers(props.projectId)
} catch (err) {
error.value = err instanceof Error ? err.message : 'Failed to load members'
+ toast({
+ title: 'Error',
+ description: 'Failed to load project members',
+ variant: 'destructive',
+ })
} finally {
isLoading.value = false
}
}
-const loadAvailableUsers = async () => {
+const loadUsers = async () => {
try {
- const allUsers = await userService.getAllUsers()
- // Filter out users who are already members
- const memberUserIds = new Set(members.value.map(m => m.user_id))
- availableUsers.value = allUsers.filter(user =>
- user.is_approved && !memberUserIds.has(user.id)
- )
+ allUsers.value = await userService.getUsers()
} catch (err) {
- console.error('Failed to load users:', err)
- error.value = 'Failed to load available users'
- availableUsers.value = [] // Ensure it's always an array
+ toast({
+ title: 'Error',
+ description: 'Failed to load users',
+ variant: 'destructive',
+ })
}
}
-const updateMemberRole = async (memberId: number, departmentRole: string | null) => {
+const updateMemberRole = async (member: ProjectMember, departmentRole: string | null) => {
try {
- isUpdatingMember.value = memberId
- await projectService.updateProjectMember(props.projectId, memberId, {
+ isUpdatingMember.value = member.id
+ const updatedMember = await projectService.updateProjectMember(props.projectId, member.id, {
department_role: departmentRole as any
})
-
- // Update local state
- const member = members.value.find(m => m.id === memberId)
- if (member) {
- member.department_role = departmentRole as any
+
+ const index = members.value.findIndex(m => m.id === member.id)
+ if (index !== -1) {
+ members.value[index] = updatedMember
}
+
+ toast({
+ title: 'Role updated',
+ description: 'Member department role has been updated',
+ })
} catch (err) {
- error.value = err instanceof Error ? err.message : 'Failed to update member role'
+ toast({
+ title: 'Error',
+ description: 'Failed to update member role',
+ variant: 'destructive',
+ })
} finally {
isUpdatingMember.value = null
}
@@ -330,18 +345,24 @@ const addMember = async () => {
isAddingMember.value = true
const memberData = {
user_id: parseInt(newMember.value.userId),
- department_role: newMember.value.departmentRole || undefined
+ department_role: newMember.value.departmentRole === 'none' ? undefined : newMember.value.departmentRole
}
-
+
const addedMember = await projectService.addProjectMember(props.projectId, memberData)
members.value.push(addedMember)
-
+
closeAddDialog()
-
- // Refresh available users
- await loadAvailableUsers()
+
+ toast({
+ title: 'Member added',
+ description: 'Team member has been added to the project',
+ })
} catch (err) {
- error.value = err instanceof Error ? err.message : 'Failed to add member'
+ toast({
+ title: 'Error',
+ description: err instanceof Error ? err.message : 'Failed to add member',
+ variant: 'destructive',
+ })
} finally {
isAddingMember.value = false
}
@@ -357,20 +378,25 @@ const confirmRemoveMember = async () => {
try {
await projectService.removeProjectMember(props.projectId, memberToRemove.value.id)
-
- // Remove from local state
+
const index = members.value.findIndex(m => m.id === memberToRemove.value!.id)
if (index !== -1) {
members.value.splice(index, 1)
}
-
+
+ toast({
+ title: 'Member removed',
+ description: 'Team member has been removed from the project',
+ })
+ } catch (err) {
+ toast({
+ title: 'Error',
+ description: 'Failed to remove member',
+ variant: 'destructive',
+ })
+ } finally {
showRemoveDialog.value = false
memberToRemove.value = null
-
- // Refresh available users
- await loadAvailableUsers()
- } catch (err) {
- error.value = err instanceof Error ? err.message : 'Failed to remove member'
}
}
@@ -378,10 +404,6 @@ const getUserInitials = (member: ProjectMember) => {
return `${member.user_first_name.charAt(0)}${member.user_last_name.charAt(0)}`.toUpperCase()
}
-import { useAvatarUrl } from '@/composables/useAvatarUrl'
-
-const { getAvatarUrl, getInitialsAvatarUrl } = useAvatarUrl()
-
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
@@ -391,25 +413,17 @@ const formatDate = (dateString: string) => {
}
const openAddDialog = async () => {
- try {
- await loadAvailableUsers()
- showAddMemberDialog.value = true
- } catch (err) {
- console.error('Failed to open add dialog:', err)
- error.value = 'Failed to load user list'
- }
+ await loadUsers()
+ showAddMemberDialog.value = true
}
const closeAddDialog = () => {
showAddMemberDialog.value = false
- // Reset form
- newMember.value = { userId: '', departmentRole: '' }
+ newMember.value = { userId: '', departmentRole: 'none' }
}
-
-
// Lifecycle
onMounted(() => {
loadMembers()
})
-
\ No newline at end of file
+
diff --git a/frontend/src/components/shot/ShotBrowser.vue b/frontend/src/components/shot/ShotBrowser.vue
index 1e34366..dcf1331 100644
--- a/frontend/src/components/shot/ShotBrowser.vue
+++ b/frontend/src/components/shot/ShotBrowser.vue
@@ -169,17 +169,26 @@
leave-from-class="translate-x-0"
leave-to-class="translate-x-full"
>
-
+
@@ -190,14 +199,22 @@
+
@@ -316,6 +333,7 @@ import ShotCard from './ShotCard.vue'
import ShotForm from './ShotForm.vue'
import BulkShotForm from './BulkShotForm.vue'
import ShotDetailPanel from './ShotDetailPanel.vue'
+import TaskDetailPanel from '@/components/task/TaskDetailPanel.vue'
import ShotsDataTable from './ShotsDataTable.vue'
import ShotTableToolbar from './ShotTableToolbar.vue'
import { createShotColumns, type ShotColumnMeta } from './columns'
@@ -845,15 +863,19 @@ const clearSearch = () => {
loadShots()
}
-const handleCreateTask = () => {
- // TODO: Navigate to task creation for this shot
- console.log('Create task for shot:', selectedShot.value?.name)
+const selectedTaskId = ref(null)
+const selectedTaskTab = ref('infos')
+
+const handleSelectTask = (task: { id: number }, tab?: string) => {
+ selectedTaskId.value = task.id
+ selectedTaskTab.value = tab || 'infos'
}
-const handleSelectTask = (task: any) => {
- // TODO: Navigate to task detail view
- console.log('View task:', task.name)
-}
+// Reset the task sub-panel whenever the shot selection changes (including close)
+watch(selectedShot, () => {
+ selectedTaskId.value = null
+ selectedTaskTab.value = 'infos'
+})
const formatStatus = (status: ShotStatus) => {
return status.split('_').map(word =>
diff --git a/frontend/src/components/shot/ShotDetailPanel.vue b/frontend/src/components/shot/ShotDetailPanel.vue
index 0faf288..03083c0 100644
--- a/frontend/src/components/shot/ShotDetailPanel.vue
+++ b/frontend/src/components/shot/ShotDetailPanel.vue
@@ -131,15 +131,58 @@
Tasks
-
-
- Add Task
-
+
+
+
+
+
+ Add Task
+
+
+
+ Add Task
+
+ All task types already added
+
+
+
+ {{ formatTaskType(type) }}
+
+
+
+
+
+
+
+
+
+
+
+
+ Publish version for task
+
+
+ {{ formatTaskType(task.task_type) }}
+
+
+
+
+
@@ -158,7 +201,7 @@
v-for="task in tasks"
:key="task.id"
class="px-4 py-3 grid grid-cols-3 gap-4 items-center hover:bg-muted/50 cursor-pointer transition-colors border-b last:border-b-0"
- @click="$emit('select-task', task)"
+ @click="$emit('select-task', task, 'infos')"
>
{{ formatTaskType(task.task_type) }}
@@ -178,15 +221,29 @@
Production Notes
-
-
- Add Note
-
+
+
+
+
+ Add Note
+
+
+
+ Add note to task
+
+
+ {{ formatTaskType(task.task_type) }}
+
+
+
+
@@ -222,15 +279,29 @@
Reference Files
-
-
- Upload Reference
-
+
+
+
+
+ Upload Reference
+
+
+
+ Upload reference to task
+
+
+ {{ formatTaskType(task.task_type) }}
+
+
+
+
@@ -279,15 +350,17 @@