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:
@@ -30,32 +30,54 @@
|
||||
<template v-if="allDepartments.length > 0">
|
||||
<div
|
||||
v-for="department in allDepartments"
|
||||
:key="department"
|
||||
class="flex items-center justify-between p-3 hover:bg-muted/50 transition-colors"
|
||||
:key="department.name"
|
||||
class="p-3 hover:bg-muted/50 transition-colors space-y-2"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="font-medium capitalize">{{ formatDepartmentName(department) }}</span>
|
||||
<Badge v-if="isStandardDepartment(department)" variant="secondary">
|
||||
Standard
|
||||
</Badge>
|
||||
<Badge v-else variant="outline">
|
||||
Custom
|
||||
</Badge>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium capitalize">{{ formatName(department.name) }}</span>
|
||||
<Badge v-if="isStandardDepartment(department.name)" variant="secondary">Standard</Badge>
|
||||
<Badge v-else variant="outline">Custom</Badge>
|
||||
<Badge variant="outline" class="capitalize">{{ department.type }}</Badge>
|
||||
</div>
|
||||
<div v-if="!isStandardDepartment(department.name)" class="flex items-center gap-2">
|
||||
<Button size="sm" variant="ghost" @click="openEditDialog(department.name)">
|
||||
<Pencil class="h-4 w-4" />
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" @click="handleDelete(department.name)">
|
||||
<Trash2 class="h-4 w-4 text-destructive" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!isStandardDepartment(department)" class="flex items-center gap-2">
|
||||
|
||||
<div class="flex flex-wrap items-center gap-1.5 pl-1">
|
||||
<Badge
|
||||
v-for="taskType in department.task_types"
|
||||
:key="taskType"
|
||||
variant="secondary"
|
||||
class="text-xs font-normal capitalize gap-1"
|
||||
>
|
||||
{{ formatName(taskType) }}
|
||||
<button
|
||||
v-if="!isStandardDepartment(department.name)"
|
||||
class="hover:text-destructive"
|
||||
@click="handleDeleteTaskType(department.name, taskType)"
|
||||
>
|
||||
<X class="h-3 w-3" />
|
||||
</button>
|
||||
</Badge>
|
||||
<span v-if="department.task_types.length === 0" class="text-xs text-muted-foreground">
|
||||
No task types defined
|
||||
</span>
|
||||
<Button
|
||||
v-if="!isStandardDepartment(department.name)"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
@click="openEditDialog(department)"
|
||||
class="h-6 px-2 text-xs"
|
||||
@click="openAddTaskTypeDialog(department.name)"
|
||||
>
|
||||
<Pencil class="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
@click="handleDelete(department)"
|
||||
>
|
||||
<Trash2 class="h-4 w-4 text-destructive" />
|
||||
<Plus class="h-3 w-3 mr-1" />
|
||||
Add Task Type
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -66,7 +88,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add/Edit Dialog -->
|
||||
<!-- Add/Edit Department Dialog -->
|
||||
<Dialog :open="isDialogOpen" @update:open="closeDialog">
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
@@ -98,6 +120,22 @@
|
||||
2-50 characters, lowercase alphanumeric with underscores only
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="dialogMode === 'add'" class="space-y-2">
|
||||
<Label>Type</Label>
|
||||
<Select v-model="departmentType">
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="shot">Shot</SelectItem>
|
||||
<SelectItem value="asset">Asset</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Shot departments apply to shot tasks; asset departments apply to asset tasks.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
@@ -112,7 +150,45 @@
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<!-- Delete Confirmation Dialog -->
|
||||
<!-- Add Task Type Dialog -->
|
||||
<Dialog :open="isTaskTypeDialogOpen" @update:open="closeTaskTypeDialog">
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add Task Type to "{{ formatName(taskTypeDepartment) }}"</DialogTitle>
|
||||
<DialogDescription>
|
||||
Enter a name for the new task type. Use lowercase letters, numbers, and underscores only.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div class="space-y-4 py-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="taskTypeName">Task Type Name</Label>
|
||||
<Input
|
||||
id="taskTypeName"
|
||||
v-model="taskTypeName"
|
||||
placeholder="e.g., blocking, first_pass"
|
||||
:class="{ 'border-destructive': taskTypeValidationError }"
|
||||
@input="validateTaskTypeName"
|
||||
/>
|
||||
<p v-if="taskTypeValidationError" class="text-sm text-destructive">
|
||||
{{ taskTypeValidationError }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="closeTaskTypeDialog">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button @click="handleAddTaskType" :disabled="!isTaskTypeNameValid || isSavingTaskType">
|
||||
<div v-if="isSavingTaskType" class="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
||||
Add
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<!-- Delete Department Confirmation Dialog -->
|
||||
<AlertDialog
|
||||
:open="isDeleteDialogOpen"
|
||||
@update:open="(open) => {
|
||||
@@ -146,16 +222,52 @@
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
<!-- Delete Task Type Confirmation Dialog -->
|
||||
<AlertDialog
|
||||
:open="isDeleteTaskTypeDialogOpen"
|
||||
@update:open="(open) => {
|
||||
isDeleteTaskTypeDialogOpen = open
|
||||
if (!open && !isDeletingTaskType) {
|
||||
taskTypeToDelete = null
|
||||
deleteTaskTypeError = ''
|
||||
}
|
||||
}"
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete Task Type</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to delete the task type "{{ taskTypeToDelete?.taskType }}" from "{{ taskTypeToDelete?.department }}"?
|
||||
<span v-if="deleteTaskTypeError" class="block mt-2 text-destructive font-medium">
|
||||
{{ deleteTaskTypeError }}
|
||||
</span>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<Button
|
||||
@click="confirmDeleteTaskType"
|
||||
:disabled="isDeletingTaskType"
|
||||
class="bg-destructive hover:bg-destructive/90"
|
||||
>
|
||||
<div v-if="isDeletingTaskType" class="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
||||
Delete
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { Users, Plus, Pencil, Trash2 } from 'lucide-vue-next'
|
||||
import { Users, Plus, Pencil, Trash2, X } from 'lucide-vue-next'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -174,7 +286,7 @@ import {
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { useDepartmentsStore } from '@/stores/departments'
|
||||
import { departmentService } from '@/services/department'
|
||||
import { departmentService, type DepartmentType } from '@/services/department'
|
||||
import { useToast } from '@/components/ui/toast/use-toast'
|
||||
|
||||
interface Props {
|
||||
@@ -193,26 +305,45 @@ const departmentsStore = useDepartmentsStore()
|
||||
// State
|
||||
const isLoading = ref(true)
|
||||
|
||||
// Dialog state
|
||||
// Department dialog state
|
||||
const isDialogOpen = ref(false)
|
||||
const dialogMode = ref<'add' | 'edit'>('add')
|
||||
const departmentName = ref('')
|
||||
const departmentType = ref<DepartmentType | ''>('')
|
||||
const originalDepartmentName = ref('')
|
||||
const validationError = ref('')
|
||||
const isSaving = ref(false)
|
||||
|
||||
// Delete dialog state
|
||||
// Task type dialog state
|
||||
const isTaskTypeDialogOpen = ref(false)
|
||||
const taskTypeDepartment = ref('')
|
||||
const taskTypeName = ref('')
|
||||
const taskTypeValidationError = ref('')
|
||||
const isSavingTaskType = ref(false)
|
||||
|
||||
// Delete department dialog state
|
||||
const isDeleteDialogOpen = ref(false)
|
||||
const departmentToDelete = ref('')
|
||||
const deleteError = ref('')
|
||||
const isDeleting = ref(false)
|
||||
|
||||
// Delete task type dialog state
|
||||
const isDeleteTaskTypeDialogOpen = ref(false)
|
||||
const taskTypeToDelete = ref<{ department: string; taskType: string } | null>(null)
|
||||
const deleteTaskTypeError = ref('')
|
||||
const isDeletingTaskType = ref(false)
|
||||
|
||||
// Computed
|
||||
const allDepartments = computed(() => departmentsStore.getAllDepartmentOptions(props.projectId))
|
||||
const allDepartments = computed(() => departmentsStore.getProjectDepartments(props.projectId)?.departments || [])
|
||||
const standardDepartments = computed(() => departmentsStore.getProjectDepartments(props.projectId)?.standard_departments || [])
|
||||
const allDepartmentNames = computed(() => allDepartments.value.map(d => d.name))
|
||||
|
||||
const isDepartmentNameValid = computed(() => {
|
||||
return departmentName.value.length >= 2 && !validationError.value
|
||||
return departmentName.value.length >= 2 && !validationError.value && (dialogMode.value === 'edit' || !!departmentType.value)
|
||||
})
|
||||
|
||||
const isTaskTypeNameValid = computed(() => {
|
||||
return taskTypeName.value.length >= 2 && !taskTypeValidationError.value
|
||||
})
|
||||
|
||||
// Methods
|
||||
@@ -233,11 +364,11 @@ const loadDepartments = async () => {
|
||||
}
|
||||
|
||||
const isStandardDepartment = (department: string): boolean => {
|
||||
return standardDepartments.value.includes(department)
|
||||
return standardDepartments.value.some(d => d.name === department)
|
||||
}
|
||||
|
||||
const formatDepartmentName = (department: string): string => {
|
||||
return department.replace(/_/g, ' ')
|
||||
const formatName = (name: string): string => {
|
||||
return name.replace(/_/g, ' ')
|
||||
}
|
||||
|
||||
const validateDepartmentName = () => {
|
||||
@@ -264,7 +395,7 @@ const validateDepartmentName = () => {
|
||||
}
|
||||
|
||||
if (dialogMode.value === 'add' || name !== originalDepartmentName.value) {
|
||||
if (allDepartments.value.includes(name)) {
|
||||
if (allDepartmentNames.value.includes(name)) {
|
||||
validationError.value = 'A department with this name already exists'
|
||||
return
|
||||
}
|
||||
@@ -273,9 +404,37 @@ const validateDepartmentName = () => {
|
||||
validationError.value = ''
|
||||
}
|
||||
|
||||
const validateTaskTypeName = () => {
|
||||
const name = taskTypeName.value.trim()
|
||||
|
||||
if (name.length === 0) {
|
||||
taskTypeValidationError.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
if (name.length < 2 || name.length > 50) {
|
||||
taskTypeValidationError.value = 'Task type name must be 2-50 characters'
|
||||
return
|
||||
}
|
||||
|
||||
if (!/^[a-z0-9_]+$/.test(name)) {
|
||||
taskTypeValidationError.value = 'Task type name must be lowercase alphanumeric with underscores only'
|
||||
return
|
||||
}
|
||||
|
||||
const department = allDepartments.value.find(d => d.name === taskTypeDepartment.value)
|
||||
if (department?.task_types.includes(name)) {
|
||||
taskTypeValidationError.value = 'A task type with this name already exists in this department'
|
||||
return
|
||||
}
|
||||
|
||||
taskTypeValidationError.value = ''
|
||||
}
|
||||
|
||||
const openAddDialog = () => {
|
||||
dialogMode.value = 'add'
|
||||
departmentName.value = ''
|
||||
departmentType.value = ''
|
||||
originalDepartmentName.value = ''
|
||||
validationError.value = ''
|
||||
isDialogOpen.value = true
|
||||
@@ -292,10 +451,25 @@ const openEditDialog = (department: string) => {
|
||||
const closeDialog = () => {
|
||||
isDialogOpen.value = false
|
||||
departmentName.value = ''
|
||||
departmentType.value = ''
|
||||
originalDepartmentName.value = ''
|
||||
validationError.value = ''
|
||||
}
|
||||
|
||||
const openAddTaskTypeDialog = (department: string) => {
|
||||
taskTypeDepartment.value = department
|
||||
taskTypeName.value = ''
|
||||
taskTypeValidationError.value = ''
|
||||
isTaskTypeDialogOpen.value = true
|
||||
}
|
||||
|
||||
const closeTaskTypeDialog = () => {
|
||||
isTaskTypeDialogOpen.value = false
|
||||
taskTypeDepartment.value = ''
|
||||
taskTypeName.value = ''
|
||||
taskTypeValidationError.value = ''
|
||||
}
|
||||
|
||||
const handleDialogSave = async () => {
|
||||
validateDepartmentName()
|
||||
|
||||
@@ -307,7 +481,10 @@ const handleDialogSave = async () => {
|
||||
isSaving.value = true
|
||||
|
||||
if (dialogMode.value === 'add') {
|
||||
const response = await departmentService.addDepartment(props.projectId, { department: departmentName.value.trim() })
|
||||
const response = await departmentService.addDepartment(props.projectId, {
|
||||
department: departmentName.value.trim(),
|
||||
department_type: departmentType.value as DepartmentType
|
||||
})
|
||||
departmentsStore.updateProjectDepartments(props.projectId, response)
|
||||
|
||||
toast({
|
||||
@@ -347,6 +524,44 @@ const handleDialogSave = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddTaskType = async () => {
|
||||
validateTaskTypeName()
|
||||
|
||||
if (!isTaskTypeNameValid.value) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
isSavingTaskType.value = true
|
||||
|
||||
const response = await departmentService.addDepartmentTaskType(
|
||||
props.projectId,
|
||||
taskTypeDepartment.value,
|
||||
taskTypeName.value.trim()
|
||||
)
|
||||
departmentsStore.updateProjectDepartments(props.projectId, response)
|
||||
|
||||
toast({
|
||||
title: 'Success',
|
||||
description: `Task type "${taskTypeName.value}" added successfully`
|
||||
})
|
||||
|
||||
emit('updated')
|
||||
closeTaskTypeDialog()
|
||||
} catch (error: any) {
|
||||
console.error('Failed to add task type:', error)
|
||||
const errorMessage = error.response?.data?.detail || 'Failed to add task type'
|
||||
taskTypeValidationError.value = errorMessage
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: errorMessage,
|
||||
variant: 'destructive'
|
||||
})
|
||||
} finally {
|
||||
isSavingTaskType.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = (department: string) => {
|
||||
departmentToDelete.value = department
|
||||
deleteError.value = ''
|
||||
@@ -403,6 +618,53 @@ const confirmDelete = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteTaskType = (department: string, taskType: string) => {
|
||||
taskTypeToDelete.value = { department, taskType }
|
||||
deleteTaskTypeError.value = ''
|
||||
isDeleteTaskTypeDialogOpen.value = true
|
||||
}
|
||||
|
||||
const confirmDeleteTaskType = async () => {
|
||||
const target = taskTypeToDelete.value
|
||||
if (!target) return
|
||||
|
||||
try {
|
||||
isDeletingTaskType.value = true
|
||||
deleteTaskTypeError.value = ''
|
||||
|
||||
const response = await departmentService.removeDepartmentTaskType(props.projectId, target.department, target.taskType)
|
||||
departmentsStore.updateProjectDepartments(props.projectId, response)
|
||||
|
||||
toast({
|
||||
title: 'Success',
|
||||
description: `Task type "${target.taskType}" deleted successfully`
|
||||
})
|
||||
|
||||
emit('updated')
|
||||
|
||||
isDeleteTaskTypeDialogOpen.value = false
|
||||
taskTypeToDelete.value = null
|
||||
deleteTaskTypeError.value = ''
|
||||
} catch (error: any) {
|
||||
console.error('Failed to delete task type:', error)
|
||||
const errorData = error.response?.data
|
||||
|
||||
if (errorData?.detail?.task_count !== undefined) {
|
||||
deleteTaskTypeError.value = `Cannot delete: ${errorData.detail.task_count} task(s) are using this task type`
|
||||
} else {
|
||||
deleteTaskTypeError.value = errorData?.detail || 'Failed to delete task type'
|
||||
}
|
||||
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: deleteTaskTypeError.value,
|
||||
variant: 'destructive'
|
||||
})
|
||||
} finally {
|
||||
isDeletingTaskType.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Lifecycle
|
||||
onMounted(() => {
|
||||
loadDepartments()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,13 +1,23 @@
|
||||
import { apiClient } from './api'
|
||||
|
||||
export type DepartmentType = 'shot' | 'asset'
|
||||
|
||||
export interface DepartmentInfo {
|
||||
name: string
|
||||
type: DepartmentType
|
||||
task_types: string[]
|
||||
}
|
||||
|
||||
export interface AllDepartmentsResponse {
|
||||
departments: string[]
|
||||
standard_departments: string[]
|
||||
custom_departments: string[]
|
||||
departments: DepartmentInfo[]
|
||||
standard_departments: DepartmentInfo[]
|
||||
custom_departments: DepartmentInfo[]
|
||||
}
|
||||
|
||||
export interface CustomDepartmentCreate {
|
||||
department: string
|
||||
department_type: DepartmentType
|
||||
task_types?: string[]
|
||||
}
|
||||
|
||||
export interface CustomDepartmentUpdate {
|
||||
@@ -22,6 +32,13 @@ export interface DepartmentInUseError {
|
||||
task_count: number
|
||||
}
|
||||
|
||||
export interface DepartmentTaskTypeInUseError {
|
||||
error: string
|
||||
department: string
|
||||
task_type: string
|
||||
task_count: number
|
||||
}
|
||||
|
||||
export const departmentService = {
|
||||
async getAllDepartments(projectId: number): Promise<AllDepartmentsResponse> {
|
||||
const response = await apiClient.get(`/projects/${projectId}/departments`)
|
||||
@@ -43,5 +60,28 @@ export const departmentService = {
|
||||
const encodedDepartment = encodeURIComponent(department)
|
||||
const response = await apiClient.delete(`/projects/${projectId}/departments/${encodedDepartment}`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
async addDepartmentTaskType(projectId: number, department: string, taskType: string): Promise<AllDepartmentsResponse> {
|
||||
const encodedDepartment = encodeURIComponent(department)
|
||||
const response = await apiClient.post(`/projects/${projectId}/departments/${encodedDepartment}/task-types`, { task_type: taskType })
|
||||
return response.data
|
||||
},
|
||||
|
||||
async renameDepartmentTaskType(projectId: number, department: string, oldTaskType: string, newTaskType: string): Promise<AllDepartmentsResponse> {
|
||||
const encodedDepartment = encodeURIComponent(department)
|
||||
const encodedTaskType = encodeURIComponent(oldTaskType)
|
||||
const response = await apiClient.put(`/projects/${projectId}/departments/${encodedDepartment}/task-types/${encodedTaskType}`, {
|
||||
old_name: oldTaskType,
|
||||
new_name: newTaskType
|
||||
})
|
||||
return response.data
|
||||
},
|
||||
|
||||
async removeDepartmentTaskType(projectId: number, department: string, taskType: string): Promise<AllDepartmentsResponse> {
|
||||
const encodedDepartment = encodeURIComponent(department)
|
||||
const encodedTaskType = encodeURIComponent(taskType)
|
||||
const response = await apiClient.delete(`/projects/${projectId}/departments/${encodedDepartment}/task-types/${encodedTaskType}`)
|
||||
return response.data
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { departmentService, type AllDepartmentsResponse } from '@/services/department'
|
||||
import { departmentService, type AllDepartmentsResponse, type DepartmentInfo, type DepartmentType } from '@/services/department'
|
||||
|
||||
interface ProjectDepartments {
|
||||
projectId: number
|
||||
@@ -45,12 +45,31 @@ export const useDepartmentsStore = defineStore('departments', () => {
|
||||
}
|
||||
})
|
||||
|
||||
// Get all department options (standard + custom) for a project
|
||||
// Get all department names (standard + custom) for a project
|
||||
const getAllDepartmentOptions = computed(() => {
|
||||
return (projectId: number): string[] => {
|
||||
const departments = getProjectDepartments.value(projectId)
|
||||
if (!departments) return []
|
||||
return departments.departments
|
||||
return departments.departments.map(d => d.name)
|
||||
}
|
||||
})
|
||||
|
||||
// Get all departments of a given type (shot or asset) for a project
|
||||
const getDepartmentsByType = computed(() => {
|
||||
return (projectId: number, type: DepartmentType): DepartmentInfo[] => {
|
||||
const departments = getProjectDepartments.value(projectId)
|
||||
if (!departments) return []
|
||||
return departments.departments.filter(d => d.type === type)
|
||||
}
|
||||
})
|
||||
|
||||
// Get the task types owned by a specific department for a project
|
||||
const getDepartmentTaskTypes = computed(() => {
|
||||
return (projectId: number, departmentName: string): string[] => {
|
||||
const departments = getProjectDepartments.value(projectId)
|
||||
if (!departments) return []
|
||||
const department = departments.departments.find(d => d.name === departmentName)
|
||||
return department?.task_types || []
|
||||
}
|
||||
})
|
||||
|
||||
@@ -128,6 +147,8 @@ export const useDepartmentsStore = defineStore('departments', () => {
|
||||
getProjectDepartments,
|
||||
isLoading,
|
||||
getAllDepartmentOptions,
|
||||
getDepartmentsByType,
|
||||
getDepartmentTaskTypes,
|
||||
|
||||
// Actions
|
||||
fetchProjectDepartments,
|
||||
|
||||
Reference in New Issue
Block a user