Add shot/asset typing and owned task types to departments
Departments now carry a type (shot or asset) and own a list of task types (e.g. Animation: blocking/primary_pass/second_pass, Composite: first_pass/second_pass, plus a new Simulation department), additive to the existing flat Custom Task Type system. When a task's type belongs to a department, its department is derived and kept in sync server-side across create/update paths; Task Type is now editable in the Task Detail panel and Department options are filtered to the task's shot/asset scope.
This commit is contained in:
@@ -119,9 +119,18 @@
|
||||
<div class="grid grid-cols-2 gap-4 text-xs">
|
||||
<div>
|
||||
<Label class="text-muted-foreground">Type</Label>
|
||||
<p class="text-sm mt-1">
|
||||
<Badge variant="outline">{{ formatTaskType(task.task_type) }}</Badge>
|
||||
</p>
|
||||
<div class="mt-1">
|
||||
<Select :model-value="task.task_type" @update:model-value="(value) => handleTaskTypeChange(value as string)">
|
||||
<SelectTrigger class="h-8">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem v-for="taskType in taskTypeOptions" :key="taskType" :value="taskType">
|
||||
{{ formatTaskType(taskType) }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label class="text-muted-foreground">Department</Label>
|
||||
@@ -356,6 +365,7 @@ import TaskAttachments from './TaskAttachments.vue'
|
||||
import TaskSubmissions from './TaskSubmissions.vue'
|
||||
import { taskService, type Task, type ProductionNote, type TaskAttachment, type Submission } from '@/services/task'
|
||||
import { projectService, type ProjectMember } from '@/services/project'
|
||||
import { customTaskTypeService } from '@/services/customTaskType'
|
||||
import { useDepartmentsStore } from '@/stores/departments'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useToast } from '@/components/ui/toast/use-toast'
|
||||
@@ -383,6 +393,8 @@ const localStatus = ref('')
|
||||
const localStartDate = ref('')
|
||||
const localDeadline = ref('')
|
||||
const localDepartment = ref('')
|
||||
const flatShotTaskTypes = ref<string[]>([])
|
||||
const flatAssetTaskTypes = ref<string[]>([])
|
||||
const notes = ref<ProductionNote[]>([])
|
||||
const attachments = ref<TaskAttachment[]>([])
|
||||
const submissions = ref<Submission[]>([])
|
||||
@@ -410,11 +422,30 @@ const canSubmitWork = computed(() => {
|
||||
|
||||
const canReassign = computed(() => isCoordinatorOrAdmin.value)
|
||||
|
||||
// Departments are type-scoped (shot vs asset); a standalone task (neither
|
||||
// shot nor asset) falls back to the unfiltered list.
|
||||
const departmentOptions = computed(() => {
|
||||
if (!task.value) return []
|
||||
if (task.value.shot_id) return departmentsStore.getDepartmentsByType(task.value.project_id, 'shot').map(d => d.name)
|
||||
if (task.value.asset_id) return departmentsStore.getDepartmentsByType(task.value.project_id, 'asset').map(d => d.name)
|
||||
return departmentsStore.getAllDepartmentOptions(task.value.project_id)
|
||||
})
|
||||
|
||||
// Task Type options come from the current department's owned task types when
|
||||
// it has any; otherwise fall back to the existing flat asset/shot task type list.
|
||||
const taskTypeOptions = computed(() => {
|
||||
if (!task.value) return []
|
||||
const departmentTaskTypes = localDepartment.value
|
||||
? departmentsStore.getDepartmentTaskTypes(task.value.project_id, localDepartment.value)
|
||||
: []
|
||||
if (departmentTaskTypes.length > 0) return departmentTaskTypes
|
||||
|
||||
const flatTypes = task.value.shot_id ? flatShotTaskTypes.value : flatAssetTaskTypes.value
|
||||
// Always include the task's current type, even if it isn't in either list
|
||||
// (e.g. a legacy or since-removed value), so the Select never shows blank.
|
||||
return flatTypes.includes(task.value.task_type) ? flatTypes : [task.value.task_type, ...flatTypes]
|
||||
})
|
||||
|
||||
function formatDepartment(department: string): string {
|
||||
return department.replace(/_/g, ' ').replace(/^\w/, c => c.toUpperCase())
|
||||
}
|
||||
@@ -429,6 +460,10 @@ async function loadTask() {
|
||||
localDeadline.value = task.value.deadline || ''
|
||||
localDepartment.value = task.value.department || ''
|
||||
departmentsStore.fetchProjectDepartments(task.value.project_id)
|
||||
customTaskTypeService.getAllTaskTypes(task.value.project_id).then(types => {
|
||||
flatShotTaskTypes.value = types.shot_task_types
|
||||
flatAssetTaskTypes.value = types.asset_task_types
|
||||
}).catch(err => console.error('Failed to load task types:', err))
|
||||
} catch (err: any) {
|
||||
console.error('Error loading task:', err)
|
||||
error.value = err.response?.data?.detail || 'Failed to load task'
|
||||
@@ -515,10 +550,24 @@ async function handleDateChange(field: 'start_date' | 'deadline', value: string)
|
||||
async function handleDepartmentChange(value: string) {
|
||||
if (!task.value) return
|
||||
|
||||
const previous = task.value.department
|
||||
const previousDepartment = task.value.department
|
||||
const previousTaskType = task.value.task_type
|
||||
|
||||
// If the current task type doesn't belong to the newly-picked department
|
||||
// (and that department has its own task types), reset to its first one so
|
||||
// department and task type stay consistent.
|
||||
const newDepartmentTaskTypes = value ? departmentsStore.getDepartmentTaskTypes(task.value.project_id, value) : []
|
||||
const needsTaskTypeReset = newDepartmentTaskTypes.length > 0 && !newDepartmentTaskTypes.includes(task.value.task_type)
|
||||
|
||||
const payload: Record<string, any> = { department: value || null }
|
||||
if (needsTaskTypeReset) {
|
||||
payload.task_type = newDepartmentTaskTypes[0]
|
||||
}
|
||||
|
||||
try {
|
||||
const updated = await taskService.updateTask(props.taskId, { department: value || null } as any)
|
||||
const updated = await taskService.updateTask(props.taskId, payload as any)
|
||||
task.value.department = updated.department
|
||||
task.value.task_type = updated.task_type
|
||||
localDepartment.value = updated.department || ''
|
||||
emit('taskUpdated')
|
||||
toast({
|
||||
@@ -527,7 +576,8 @@ async function handleDepartmentChange(value: string) {
|
||||
})
|
||||
} catch (error: any) {
|
||||
console.error('Error updating department:', error)
|
||||
localDepartment.value = previous || ''
|
||||
localDepartment.value = previousDepartment || ''
|
||||
task.value.task_type = previousTaskType
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: error.response?.data?.detail || 'Failed to update task department',
|
||||
@@ -536,6 +586,35 @@ async function handleDepartmentChange(value: string) {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleTaskTypeChange(value: string) {
|
||||
if (!task.value || value === task.value.task_type) return
|
||||
|
||||
const previousTaskType = task.value.task_type
|
||||
const previousDepartment = task.value.department
|
||||
|
||||
try {
|
||||
const updated = await taskService.updateTask(props.taskId, { task_type: value } as any)
|
||||
task.value.task_type = updated.task_type
|
||||
task.value.department = updated.department
|
||||
localDepartment.value = updated.department || ''
|
||||
emit('taskUpdated')
|
||||
toast({
|
||||
title: 'Success',
|
||||
description: 'Task type updated successfully'
|
||||
})
|
||||
} catch (error: any) {
|
||||
console.error('Error updating task type:', error)
|
||||
task.value.task_type = previousTaskType
|
||||
task.value.department = previousDepartment
|
||||
localDepartment.value = previousDepartment || ''
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: error.response?.data?.detail || 'Failed to update task type',
|
||||
variant: 'destructive'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function handleQuickAction(action: 'start' | 'submit') {
|
||||
if (!task.value) return
|
||||
|
||||
|
||||
Reference in New Issue
Block a user