Compare commits

...

2 Commits

Author SHA1 Message Date
indigo 3d67c304a3 Merge department task types into the Task Type management page
Project Settings > Tasks previously only showed the flat custom task
type lists, with department-owned task types manageable only from the
separate Departments tab. The Asset/Shot Task Type sections now show a
merged view (flat + department-owned, tagged with their department),
and the Add/Edit/Delete dialog can create a task type directly under a
custom department instead of just as a general-purpose flat type.
2026-07-24 22:45:25 +08:00
indigo e1dd5c6eae Offer department-owned task types in the shot/asset Add Task dropdowns
ShotDetailPanel and AssetDetailPanel now merge department task types
(filtered to shot/asset departments respectively) into the flat task
type list shown in "Add Task", so types like Animation's blocking or
Composite's first_pass are selectable at creation time instead of only
via editing a task's type afterward. No backend change needed since
task creation already derives department from an owned task type.
2026-07-24 22:45:17 +08:00
3 changed files with 201 additions and 99 deletions
@@ -293,6 +293,7 @@ import { assetService, AssetStatus, type Asset, type TaskStatusInfo } from '@/se
import { taskService, type ProductionNote, type Submission } from '@/services/task' import { taskService, type ProductionNote, type Submission } from '@/services/task'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
import { useDepartmentsStore } from '@/stores/departments'
interface Task { interface Task {
id: number id: number
@@ -321,6 +322,7 @@ const emit = defineEmits<Emits>()
const authStore = useAuthStore() const authStore = useAuthStore()
const userStore = useUserStore() const userStore = useUserStore()
const departmentsStore = useDepartmentsStore()
// Reactive state // Reactive state
const asset = ref<Asset | null>(null) const asset = ref<Asset | null>(null)
@@ -363,9 +365,15 @@ const progressPercentage = computed(() => {
return Math.round((completedTasksCount.value / tasks.value.length) * 100) return Math.round((completedTasksCount.value / tasks.value.length) * 100)
}) })
// Task types offered in "Add Task" include the flat asset task type list
// plus every task type owned by an asset department (e.g. Modeling's own
// task types), deduped against types the asset already has a task for.
const availableTaskTypes = computed(() => { const availableTaskTypes = computed(() => {
const existingTypes = new Set(tasks.value.map(task => task.task_type)) const existingTypes = new Set(tasks.value.map(task => task.task_type))
return props.allTaskTypes.filter(type => !existingTypes.has(type)) const departmentTaskTypes = departmentsStore.getDepartmentsByType(props.projectId, 'asset')
.flatMap(d => d.task_types)
const merged = Array.from(new Set([...props.allTaskTypes, ...departmentTaskTypes]))
return merged.filter(type => !existingTypes.has(type))
}) })
const taskStatusCounts = computed(() => { const taskStatusCounts = computed(() => {
@@ -394,6 +402,7 @@ const loadAssetDetails = async () => {
isLoading.value = true isLoading.value = true
error.value = null error.value = null
asset.value = await assetService.getAsset(props.assetId) asset.value = await assetService.getAsset(props.assetId)
departmentsStore.fetchProjectDepartments(props.projectId)
// Load users if not already loaded (for user name resolution) // Load users if not already loaded (for user name resolution)
if (userStore.users.length === 0) { if (userStore.users.length === 0) {
@@ -2,9 +2,9 @@
<div class="space-y-6"> <div class="space-y-6">
<!-- Header --> <!-- Header -->
<div> <div>
<h3 class="text-lg font-semibold">Custom Task Types</h3> <h3 class="text-lg font-semibold">Task Types</h3>
<p class="text-sm text-muted-foreground mt-1"> <p class="text-sm text-muted-foreground mt-1">
Add custom task types beyond the standard types to adapt the pipeline to your project needs Add task types beyond the standard types, either general-purpose or owned by a specific department, to adapt the pipeline to your project needs
</p> </p>
</div> </div>
@@ -30,33 +30,30 @@
<!-- Asset Task Types List --> <!-- Asset Task Types List -->
<div class="border rounded-lg divide-y"> <div class="border rounded-lg divide-y">
<template v-if="assetTaskTypes.length > 0"> <template v-if="assetTaskTypeRows.length > 0">
<div <div
v-for="taskType in assetTaskTypes" v-for="row in assetTaskTypeRows"
:key="taskType" :key="row.name"
class="flex items-center justify-between p-3 hover:bg-muted/50 transition-colors" class="flex items-center justify-between p-3 hover:bg-muted/50 transition-colors"
> >
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<span class="font-medium capitalize">{{ formatTaskTypeName(taskType) }}</span> <span class="font-medium capitalize">{{ formatTaskTypeName(row.name) }}</span>
<Badge v-if="isStandardAssetType(taskType)" variant="secondary"> <Badge v-if="row.isStandard" variant="secondary">Standard</Badge>
Standard <Badge v-else variant="outline">Custom</Badge>
</Badge> <Badge v-if="row.department" variant="outline" class="capitalize">{{ formatTaskTypeName(row.department) }}</Badge>
<Badge v-else variant="outline">
Custom
</Badge>
</div> </div>
<div v-if="!isStandardAssetType(taskType)" class="flex items-center gap-2"> <div v-if="!row.isStandard" class="flex items-center gap-2">
<Button <Button
size="sm" size="sm"
variant="ghost" variant="ghost"
@click="openEditDialog('asset', taskType)" @click="openEditDialog('asset', row.name, row.department)"
> >
<Pencil class="h-4 w-4" /> <Pencil class="h-4 w-4" />
</Button> </Button>
<Button <Button
size="sm" size="sm"
variant="ghost" variant="ghost"
@click="handleDelete('asset', taskType)" @click="handleDelete('asset', row.name, row.department)"
> >
<Trash2 class="h-4 w-4 text-destructive" /> <Trash2 class="h-4 w-4 text-destructive" />
</Button> </Button>
@@ -86,33 +83,30 @@
<!-- Shot Task Types List --> <!-- Shot Task Types List -->
<div class="border rounded-lg divide-y"> <div class="border rounded-lg divide-y">
<template v-if="shotTaskTypes.length > 0"> <template v-if="shotTaskTypeRows.length > 0">
<div <div
v-for="taskType in shotTaskTypes" v-for="row in shotTaskTypeRows"
:key="taskType" :key="row.name"
class="flex items-center justify-between p-3 hover:bg-muted/50 transition-colors" class="flex items-center justify-between p-3 hover:bg-muted/50 transition-colors"
> >
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<span class="font-medium capitalize">{{ formatTaskTypeName(taskType) }}</span> <span class="font-medium capitalize">{{ formatTaskTypeName(row.name) }}</span>
<Badge v-if="isStandardShotType(taskType)" variant="secondary"> <Badge v-if="row.isStandard" variant="secondary">Standard</Badge>
Standard <Badge v-else variant="outline">Custom</Badge>
</Badge> <Badge v-if="row.department" variant="outline" class="capitalize">{{ formatTaskTypeName(row.department) }}</Badge>
<Badge v-else variant="outline">
Custom
</Badge>
</div> </div>
<div v-if="!isStandardShotType(taskType)" class="flex items-center gap-2"> <div v-if="!row.isStandard" class="flex items-center gap-2">
<Button <Button
size="sm" size="sm"
variant="ghost" variant="ghost"
@click="openEditDialog('shot', taskType)" @click="openEditDialog('shot', row.name, row.department)"
> >
<Pencil class="h-4 w-4" /> <Pencil class="h-4 w-4" />
</Button> </Button>
<Button <Button
size="sm" size="sm"
variant="ghost" variant="ghost"
@click="handleDelete('shot', taskType)" @click="handleDelete('shot', row.name, row.department)"
> >
<Trash2 class="h-4 w-4 text-destructive" /> <Trash2 class="h-4 w-4 text-destructive" />
</Button> </Button>
@@ -155,7 +149,25 @@
{{ validationError }} {{ validationError }}
</p> </p>
<p v-else class="text-sm text-muted-foreground"> <p v-else class="text-sm text-muted-foreground">
3-50 characters, lowercase alphanumeric with underscores only {{ dialogDepartment ? '2-50' : '3-50' }} characters, lowercase alphanumeric with underscores only
</p>
</div>
<div v-if="dialogMode === 'add'" class="space-y-2">
<Label>Department (optional)</Label>
<Select :model-value="dialogDepartment || 'none'" @update:model-value="(value) => { dialogDepartment = value === 'none' ? '' : (value as string); validateTaskTypeName() }">
<SelectTrigger>
<SelectValue placeholder="None (general purpose)" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">None (general purpose)</SelectItem>
<SelectItem v-for="department in dialogCustomDepartments" :key="department.name" :value="department.name">
{{ formatTaskTypeName(department.name) }}
</SelectItem>
</SelectContent>
</Select>
<p class="text-sm text-muted-foreground">
Assign this task type to a custom department so it's owned by that department, or leave it general-purpose.
</p> </p>
</div> </div>
</div> </div>
@@ -181,6 +193,7 @@
// Only clear values when dialog closes and we're not in the middle of deleting // Only clear values when dialog closes and we're not in the middle of deleting
taskTypeToDelete = '' taskTypeToDelete = ''
categoryToDelete = '' categoryToDelete = ''
departmentToDelete = ''
deleteError = '' deleteError = ''
} }
}" }"
@@ -218,6 +231,7 @@ import { Badge } from '@/components/ui/badge'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { Separator } from '@/components/ui/separator' import { Separator } from '@/components/ui/separator'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -237,6 +251,8 @@ import {
AlertDialogTitle, AlertDialogTitle,
} from '@/components/ui/alert-dialog' } from '@/components/ui/alert-dialog'
import { customTaskTypeService, type AllTaskTypesResponse } from '@/services/customTaskType' import { customTaskTypeService, type AllTaskTypesResponse } from '@/services/customTaskType'
import { departmentService, type DepartmentInfo } from '@/services/department'
import { useDepartmentsStore } from '@/stores/departments'
import { useToast } from '@/components/ui/toast/use-toast' import { useToast } from '@/components/ui/toast/use-toast'
interface Props { interface Props {
@@ -250,6 +266,13 @@ const emit = defineEmits<{
}>() }>()
const { toast } = useToast() const { toast } = useToast()
const departmentsStore = useDepartmentsStore()
interface TaskTypeRow {
name: string
isStandard: boolean
department?: string
}
// State // State
const isLoading = ref(true) const isLoading = ref(true)
@@ -259,6 +282,8 @@ const taskTypes = ref<AllTaskTypesResponse | null>(null)
const isDialogOpen = ref(false) const isDialogOpen = ref(false)
const dialogMode = ref<'add' | 'edit'>('add') const dialogMode = ref<'add' | 'edit'>('add')
const dialogCategory = ref<'asset' | 'shot'>('asset') const dialogCategory = ref<'asset' | 'shot'>('asset')
const dialogDepartment = ref('')
const editingDepartment = ref('')
const taskTypeName = ref('') const taskTypeName = ref('')
const originalTaskTypeName = ref('') const originalTaskTypeName = ref('')
const validationError = ref('') const validationError = ref('')
@@ -268,6 +293,7 @@ const isSaving = ref(false)
const isDeleteDialogOpen = ref(false) const isDeleteDialogOpen = ref(false)
const taskTypeToDelete = ref('') const taskTypeToDelete = ref('')
const categoryToDelete = ref<'asset' | 'shot'>('asset') const categoryToDelete = ref<'asset' | 'shot'>('asset')
const departmentToDelete = ref('')
const deleteError = ref('') const deleteError = ref('')
const isDeleting = ref(false) const isDeleting = ref(false)
@@ -277,15 +303,52 @@ const shotTaskTypes = computed(() => taskTypes.value?.shot_task_types || [])
const standardAssetTypes = computed(() => taskTypes.value?.standard_asset_types || []) const standardAssetTypes = computed(() => taskTypes.value?.standard_asset_types || [])
const standardShotTypes = computed(() => taskTypes.value?.standard_shot_types || []) const standardShotTypes = computed(() => taskTypes.value?.standard_shot_types || [])
const allDepartments = computed(() => departmentsStore.getProjectDepartments(props.projectId)?.departments || [])
const standardDepartmentNames = computed(() => (departmentsStore.getProjectDepartments(props.projectId)?.standard_departments || []).map(d => d.name))
const assetDepartments = computed(() => allDepartments.value.filter(d => d.type === 'asset'))
const shotDepartments = computed(() => allDepartments.value.filter(d => d.type === 'shot'))
const dialogCustomDepartments = computed(() => {
const departments = dialogCategory.value === 'asset' ? assetDepartments.value : shotDepartments.value
return departments.filter(d => !standardDepartmentNames.value.includes(d.name))
})
function buildRows(flatAll: string[], flatStandard: string[], departments: DepartmentInfo[]): TaskTypeRow[] {
const rows = new Map<string, TaskTypeRow>()
for (const t of flatAll) {
rows.set(t, { name: t, isStandard: flatStandard.includes(t) })
}
for (const department of departments) {
const isDeptStandard = standardDepartmentNames.value.includes(department.name)
for (const t of department.task_types) {
const existing = rows.get(t)
if (existing) {
existing.department = department.name
existing.isStandard = existing.isStandard || isDeptStandard
} else {
rows.set(t, { name: t, isStandard: isDeptStandard, department: department.name })
}
}
}
return Array.from(rows.values())
}
const assetTaskTypeRows = computed(() => buildRows(assetTaskTypes.value, standardAssetTypes.value, assetDepartments.value))
const shotTaskTypeRows = computed(() => buildRows(shotTaskTypes.value, standardShotTypes.value, shotDepartments.value))
const isTaskTypeNameValid = computed(() => { const isTaskTypeNameValid = computed(() => {
return taskTypeName.value.length >= 3 && !validationError.value const minLength = dialogDepartment.value ? 2 : 3
return taskTypeName.value.length >= minLength && !validationError.value
}) })
// Methods // Methods
const loadTaskTypes = async () => { const loadTaskTypes = async () => {
try { try {
isLoading.value = true isLoading.value = true
taskTypes.value = await customTaskTypeService.getAllTaskTypes(props.projectId) const [types] = await Promise.all([
customTaskTypeService.getAllTaskTypes(props.projectId),
departmentsStore.fetchProjectDepartments(props.projectId, true)
])
taskTypes.value = types
} catch (error: any) { } catch (error: any) {
console.error('Failed to load task types:', error) console.error('Failed to load task types:', error)
toast({ toast({
@@ -298,28 +361,21 @@ const loadTaskTypes = async () => {
} }
} }
const isStandardAssetType = (taskType: string): boolean => {
return standardAssetTypes.value.includes(taskType)
}
const isStandardShotType = (taskType: string): boolean => {
return standardShotTypes.value.includes(taskType)
}
const formatTaskTypeName = (taskType: string): string => { const formatTaskTypeName = (taskType: string): string => {
return taskType.replace(/_/g, ' ') return taskType.replace(/_/g, ' ')
} }
const validateTaskTypeName = () => { const validateTaskTypeName = () => {
const name = taskTypeName.value.trim() const name = taskTypeName.value.trim()
const minLength = dialogDepartment.value ? 2 : 3
if (name.length === 0) { if (name.length === 0) {
validationError.value = '' validationError.value = ''
return return
} }
if (name.length < 3) { if (name.length < minLength) {
validationError.value = 'Task type name must be at least 3 characters' validationError.value = `Task type name must be at least ${minLength} characters`
return return
} }
@@ -333,10 +389,10 @@ const validateTaskTypeName = () => {
return return
} }
// Check for duplicates (only if adding or changing name) // Check for duplicates against the merged (flat + department) rows for this category
if (dialogMode.value === 'add' || name !== originalTaskTypeName.value) { if (dialogMode.value === 'add' || name !== originalTaskTypeName.value) {
const existingTypes = dialogCategory.value === 'asset' ? assetTaskTypes.value : shotTaskTypes.value const existingRows = dialogCategory.value === 'asset' ? assetTaskTypeRows.value : shotTaskTypeRows.value
if (existingTypes.includes(name)) { if (existingRows.some(row => row.name === name)) {
validationError.value = 'A task type with this name already exists' validationError.value = 'A task type with this name already exists'
return return
} }
@@ -348,15 +404,17 @@ const validateTaskTypeName = () => {
const openAddDialog = (category: 'asset' | 'shot') => { const openAddDialog = (category: 'asset' | 'shot') => {
dialogMode.value = 'add' dialogMode.value = 'add'
dialogCategory.value = category dialogCategory.value = category
dialogDepartment.value = ''
taskTypeName.value = '' taskTypeName.value = ''
originalTaskTypeName.value = '' originalTaskTypeName.value = ''
validationError.value = '' validationError.value = ''
isDialogOpen.value = true isDialogOpen.value = true
} }
const openEditDialog = (category: 'asset' | 'shot', taskType: string) => { const openEditDialog = (category: 'asset' | 'shot', taskType: string, department?: string) => {
dialogMode.value = 'edit' dialogMode.value = 'edit'
dialogCategory.value = category dialogCategory.value = category
editingDepartment.value = department || ''
taskTypeName.value = taskType taskTypeName.value = taskType
originalTaskTypeName.value = taskType originalTaskTypeName.value = taskType
validationError.value = '' validationError.value = ''
@@ -365,6 +423,8 @@ const openEditDialog = (category: 'asset' | 'shot', taskType: string) => {
const closeDialog = () => { const closeDialog = () => {
isDialogOpen.value = false isDialogOpen.value = false
dialogDepartment.value = ''
editingDepartment.value = ''
taskTypeName.value = '' taskTypeName.value = ''
originalTaskTypeName.value = '' originalTaskTypeName.value = ''
validationError.value = '' validationError.value = ''
@@ -381,17 +441,30 @@ const handleDialogSave = async () => {
isSaving.value = true isSaving.value = true
if (dialogMode.value === 'add') { if (dialogMode.value === 'add') {
if (dialogDepartment.value) {
const response = await departmentService.addDepartmentTaskType(props.projectId, dialogDepartment.value, taskTypeName.value.trim())
departmentsStore.updateProjectDepartments(props.projectId, response)
} else {
const response = await customTaskTypeService.addCustomTaskType(props.projectId, { const response = await customTaskTypeService.addCustomTaskType(props.projectId, {
task_type: taskTypeName.value.trim(), task_type: taskTypeName.value.trim(),
category: dialogCategory.value category: dialogCategory.value
}) })
console.log('Add task type response:', response)
taskTypes.value = response taskTypes.value = response
}
toast({ toast({
title: 'Success', title: 'Success',
description: `Task type "${taskTypeName.value}" added successfully` description: `Task type "${taskTypeName.value}" added successfully`
}) })
} else {
if (editingDepartment.value) {
const response = await departmentService.renameDepartmentTaskType(
props.projectId,
editingDepartment.value,
originalTaskTypeName.value,
taskTypeName.value.trim()
)
departmentsStore.updateProjectDepartments(props.projectId, response)
} else { } else {
const response = await customTaskTypeService.updateCustomTaskType( const response = await customTaskTypeService.updateCustomTaskType(
props.projectId, props.projectId,
@@ -402,8 +475,8 @@ const handleDialogSave = async () => {
category: dialogCategory.value category: dialogCategory.value
} }
) )
console.log('Update task type response:', response)
taskTypes.value = response taskTypes.value = response
}
toast({ toast({
title: 'Success', title: 'Success',
@@ -427,9 +500,10 @@ const handleDialogSave = async () => {
} }
} }
const handleDelete = (category: 'asset' | 'shot', taskType: string) => { const handleDelete = (category: 'asset' | 'shot', taskType: string, department?: string) => {
taskTypeToDelete.value = taskType taskTypeToDelete.value = taskType
categoryToDelete.value = category categoryToDelete.value = category
departmentToDelete.value = department || ''
deleteError.value = '' deleteError.value = ''
isDeleteDialogOpen.value = true isDeleteDialogOpen.value = true
} }
@@ -443,6 +517,7 @@ const confirmDelete = async () => {
// Capture values immediately before any async operations // Capture values immediately before any async operations
const taskTypeToDeleteLocal = taskTypeToDelete.value const taskTypeToDeleteLocal = taskTypeToDelete.value
const categoryToDeleteLocal = categoryToDelete.value const categoryToDeleteLocal = categoryToDelete.value
const departmentToDeleteLocal = departmentToDelete.value
try { try {
isDeleting.value = true isDeleting.value = true
@@ -454,12 +529,17 @@ const confirmDelete = async () => {
return return
} }
if (departmentToDeleteLocal) {
const response = await departmentService.removeDepartmentTaskType(props.projectId, departmentToDeleteLocal, taskTypeToDeleteLocal)
departmentsStore.updateProjectDepartments(props.projectId, response)
} else {
const response = await customTaskTypeService.deleteCustomTaskType( const response = await customTaskTypeService.deleteCustomTaskType(
props.projectId, props.projectId,
taskTypeToDeleteLocal, taskTypeToDeleteLocal,
categoryToDeleteLocal categoryToDeleteLocal
) )
taskTypes.value = response taskTypes.value = response
}
toast({ toast({
title: 'Success', title: 'Success',
@@ -471,16 +551,20 @@ const confirmDelete = async () => {
// Close dialog and clear values // Close dialog and clear values
isDeleteDialogOpen.value = false isDeleteDialogOpen.value = false
taskTypeToDelete.value = '' taskTypeToDelete.value = ''
categoryToDelete.value = '' categoryToDelete.value = 'asset'
departmentToDelete.value = ''
deleteError.value = '' deleteError.value = ''
} catch (error: any) { } catch (error: any) {
console.error('Failed to delete task type:', error) console.error('Failed to delete task type:', error)
const errorData = error.response?.data const errorData = error.response?.data
const detail = errorData?.detail
if (errorData?.task_count) { if (detail?.task_count) {
deleteError.value = `Cannot delete: ${errorData.task_count} task(s) are using this type` deleteError.value = `Cannot delete: ${detail.task_count} task(s) are using this type`
} else if (typeof detail === 'string') {
deleteError.value = detail
} else { } else {
deleteError.value = errorData?.detail || 'Failed to delete task type' deleteError.value = 'Failed to delete task type'
} }
toast({ toast({
@@ -341,6 +341,7 @@ import { shotService, type Shot, type TaskStatusInfo } from '@/services/shot'
import { taskService, type ProductionNote, type Submission } from '@/services/task' import { taskService, type ProductionNote, type Submission } from '@/services/task'
import { projectService, type ProjectMember } from '@/services/project' import { projectService, type ProjectMember } from '@/services/project'
import { useTaskStatusesStore } from '@/stores/taskStatuses' import { useTaskStatusesStore } from '@/stores/taskStatuses'
import { useDepartmentsStore } from '@/stores/departments'
import { useAvatarUrl } from '@/composables/useAvatarUrl' import { useAvatarUrl } from '@/composables/useAvatarUrl'
import { usePermission } from '@/composables/usePermission' import { usePermission } from '@/composables/usePermission'
@@ -375,6 +376,7 @@ const props = defineProps<Props>()
const emit = defineEmits<Emits>() const emit = defineEmits<Emits>()
const taskStatusesStore = useTaskStatusesStore() const taskStatusesStore = useTaskStatusesStore()
const departmentsStore = useDepartmentsStore()
const { getAvatarUrl } = useAvatarUrl() const { getAvatarUrl } = useAvatarUrl()
const { isAdmin, isCoordinatorOrAdmin } = usePermission() const { isAdmin, isCoordinatorOrAdmin } = usePermission()
@@ -435,9 +437,15 @@ const canUploadReferences = computed(() => {
const canEditDesign = computed(() => isCoordinatorOrAdmin.value) const canEditDesign = computed(() => isCoordinatorOrAdmin.value)
// Task types offered in "Add Task" include the flat shot task type list
// plus every task type owned by a shot department (e.g. Animation's own
// task types), deduped against types the shot already has a task for.
const availableTaskTypes = computed(() => { const availableTaskTypes = computed(() => {
const existingTypes = new Set(tasks.value.map(task => task.task_type)) const existingTypes = new Set(tasks.value.map(task => task.task_type))
return props.allTaskTypes.filter(type => !existingTypes.has(type)) const departmentTaskTypes = departmentsStore.getDepartmentsByType(props.projectId, 'shot')
.flatMap(d => d.task_types)
const merged = Array.from(new Set([...props.allTaskTypes, ...departmentTaskTypes]))
return merged.filter(type => !existingTypes.has(type))
}) })
// Methods // Methods
@@ -446,6 +454,7 @@ 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)
departmentsStore.fetchProjectDepartments(props.projectId)
await Promise.all([ await Promise.all([
taskStatusesStore.fetchProjectStatuses(props.projectId), taskStatusesStore.fetchProjectStatuses(props.projectId),
loadProjectMembers() loadProjectMembers()