Add group-by toggle (Task Type / Shot) to the Schedule Gantt chart
Generalizes the previous task-type-only grouping into rowGroups, keyed by either task_type or the shot/asset name depending on a new toolbar toggle. Each row's label shows whichever dimension isn't already the group header, so switching modes stays readable instead of showing redundant text. Collapse state resets on mode/project change to avoid stale group keys. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,19 @@
|
||||
{{ hasCollapsedGroups ? 'Expand All' : 'Collapse All' }}
|
||||
</Button>
|
||||
|
||||
<div class="flex items-center gap-1 border rounded-md p-0.5">
|
||||
<Button
|
||||
v-for="option in GROUP_BY_OPTIONS"
|
||||
:key="option"
|
||||
:variant="groupBy === option ? 'secondary' : 'ghost'"
|
||||
size="sm"
|
||||
class="h-7 px-2 text-xs"
|
||||
@click="groupBy = option"
|
||||
>
|
||||
{{ groupByLabel(option) }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-1 border rounded-md p-0.5">
|
||||
<Button
|
||||
v-for="scale in SCALES"
|
||||
@@ -73,7 +86,7 @@
|
||||
{{ unscheduledCount === 1 ? "isn't" : "aren't" }} shown on the chart.
|
||||
</div>
|
||||
|
||||
<div v-if="taskTypeGroups.length === 0" class="flex-1 p-12 text-center text-sm text-muted-foreground">
|
||||
<div v-if="rowGroups.length === 0" class="flex-1 p-12 text-center text-sm text-muted-foreground">
|
||||
No scheduled tasks to display yet. Set a start date and deadline on a task to see it here.
|
||||
</div>
|
||||
|
||||
@@ -94,24 +107,24 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-for="group in taskTypeGroups" :key="group.taskType">
|
||||
<div v-for="group in rowGroups" :key="group.key">
|
||||
<div
|
||||
class="flex items-center border-b bg-muted/40 cursor-pointer select-none"
|
||||
:style="{ height: GROUP_ROW_HEIGHT + 'px' }"
|
||||
@click="toggleGroup(group.taskType)"
|
||||
@click="toggleGroup(group.key)"
|
||||
>
|
||||
<div class="border-r px-3 text-xs font-medium flex items-center gap-1 self-stretch flex-shrink-0" :style="{ width: LABEL_COLUMN_WIDTH + 'px' }">
|
||||
<component
|
||||
:is="isCollapsed(group.taskType) ? ChevronRight : ChevronDown"
|
||||
:is="isCollapsed(group.key) ? ChevronRight : ChevronDown"
|
||||
class="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground"
|
||||
/>
|
||||
<span class="truncate">{{ formatTaskType(group.taskType) }}</span>
|
||||
<span class="truncate">{{ group.label }}</span>
|
||||
<span class="text-muted-foreground flex-shrink-0">({{ group.tasks.length }})</span>
|
||||
</div>
|
||||
<div class="flex-shrink-0" :style="{ width: STATUS_COLUMN_WIDTH + 'px' }"></div>
|
||||
</div>
|
||||
|
||||
<template v-if="!isCollapsed(group.taskType)">
|
||||
<template v-if="!isCollapsed(group.key)">
|
||||
<div
|
||||
v-for="task in group.tasks"
|
||||
:key="task.id"
|
||||
@@ -119,7 +132,7 @@
|
||||
:style="{ height: TASK_ROW_HEIGHT + 'px' }"
|
||||
>
|
||||
<div class="border-r pl-8 pr-3 text-xs truncate self-stretch flex items-center flex-shrink-0" :style="{ width: LABEL_COLUMN_WIDTH + 'px' }">
|
||||
{{ task.shot_name || task.asset_name || task.name }}
|
||||
{{ taskRowLabel(task) }}
|
||||
</div>
|
||||
<div class="border-r px-3 flex items-center self-stretch flex-shrink-0" :style="{ width: STATUS_COLUMN_WIDTH + 'px' }">
|
||||
<EditableTaskStatus
|
||||
@@ -170,11 +183,11 @@
|
||||
></div>
|
||||
|
||||
<!-- Rows -->
|
||||
<div v-for="group in taskTypeGroups" :key="group.taskType">
|
||||
<div v-for="group in rowGroups" :key="group.key">
|
||||
<div
|
||||
class="relative border-b bg-muted/40 cursor-pointer"
|
||||
:style="{ height: GROUP_ROW_HEIGHT + 'px' }"
|
||||
@click="toggleGroup(group.taskType)"
|
||||
@click="toggleGroup(group.key)"
|
||||
>
|
||||
<div
|
||||
v-if="group.barLeft !== null"
|
||||
@@ -183,7 +196,7 @@
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<template v-if="!isCollapsed(group.taskType)">
|
||||
<template v-if="!isCollapsed(group.key)">
|
||||
<div
|
||||
v-for="task in group.tasks"
|
||||
:key="task.id"
|
||||
@@ -215,7 +228,7 @@
|
||||
class="absolute left-full top-1/2 -translate-y-1/2 ml-1.5 text-[10px] text-foreground whitespace-nowrap pointer-events-none transition-opacity"
|
||||
:class="isTaskActive(task) ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'"
|
||||
>
|
||||
{{ task.shot_name || task.asset_name || task.name }}
|
||||
{{ taskRowLabel(task) }}
|
||||
</span>
|
||||
</div>
|
||||
<!-- Submission date markers -->
|
||||
@@ -539,23 +552,43 @@ const todayLeft = computed(() => {
|
||||
return dateToLeft(todayUtc)
|
||||
})
|
||||
|
||||
interface TaskTypeGroup {
|
||||
taskType: string
|
||||
const GROUP_BY_OPTIONS = ['taskType', 'shot'] as const
|
||||
type GroupBy = typeof GROUP_BY_OPTIONS[number]
|
||||
const groupBy = ref<GroupBy>('taskType')
|
||||
|
||||
function groupByLabel(mode: GroupBy): string {
|
||||
return mode === 'taskType' ? 'Task Type' : 'Shot'
|
||||
}
|
||||
|
||||
function entityName(task: TaskListItem): string {
|
||||
return task.shot_name || task.asset_name || task.name
|
||||
}
|
||||
|
||||
// The row label shows whichever dimension ISN'T already the group header:
|
||||
// task type when grouped by shot, and the shot/asset name when grouped by task type.
|
||||
function taskRowLabel(task: TaskListItem): string {
|
||||
return groupBy.value === 'taskType' ? entityName(task) : formatTaskType(task.task_type)
|
||||
}
|
||||
|
||||
interface RowGroup {
|
||||
key: string
|
||||
label: string
|
||||
tasks: TaskListItem[]
|
||||
barLeft: number | null
|
||||
barWidth: number | null
|
||||
}
|
||||
|
||||
const taskTypeGroups = computed<TaskTypeGroup[]>(() => {
|
||||
const byType = new Map<string, TaskListItem[]>()
|
||||
const rowGroups = computed<RowGroup[]>(() => {
|
||||
const byKey = new Map<string, TaskListItem[]>()
|
||||
for (const t of scheduledTasks.value) {
|
||||
if (!byType.has(t.task_type)) byType.set(t.task_type, [])
|
||||
byType.get(t.task_type)!.push(t)
|
||||
const key = groupBy.value === 'taskType' ? t.task_type : entityName(t)
|
||||
if (!byKey.has(key)) byKey.set(key, [])
|
||||
byKey.get(key)!.push(t)
|
||||
}
|
||||
|
||||
return Array.from(byType.entries())
|
||||
return Array.from(byKey.entries())
|
||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||
.map(([taskType, groupTasks]) => {
|
||||
.map(([key, groupTasks]) => {
|
||||
const sorted = [...groupTasks].sort((a, b) => (a.start_date || '').localeCompare(b.start_date || ''))
|
||||
let barLeft: number | null = null
|
||||
let barWidth: number | null = null
|
||||
@@ -571,18 +604,19 @@ const taskTypeGroups = computed<TaskTypeGroup[]>(() => {
|
||||
barLeft = dateToLeft(minStart!)
|
||||
barWidth = Math.max(dateToLeft(maxEnd!) - barLeft, 4)
|
||||
}
|
||||
return { taskType, tasks: sorted, barLeft, barWidth }
|
||||
const label = groupBy.value === 'taskType' ? formatTaskType(key) : key
|
||||
return { key, label, tasks: sorted, barLeft, barWidth }
|
||||
})
|
||||
})
|
||||
|
||||
function isCollapsed(taskType: string): boolean {
|
||||
return collapsedGroups.value.has(taskType)
|
||||
function isCollapsed(key: string): boolean {
|
||||
return collapsedGroups.value.has(key)
|
||||
}
|
||||
|
||||
function toggleGroup(taskType: string) {
|
||||
function toggleGroup(key: string) {
|
||||
const next = new Set(collapsedGroups.value)
|
||||
if (next.has(taskType)) next.delete(taskType)
|
||||
else next.add(taskType)
|
||||
if (next.has(key)) next.delete(key)
|
||||
else next.add(key)
|
||||
collapsedGroups.value = next
|
||||
}
|
||||
|
||||
@@ -591,9 +625,13 @@ const hasCollapsedGroups = computed(() => collapsedGroups.value.size > 0)
|
||||
function toggleAllGroups() {
|
||||
collapsedGroups.value = hasCollapsedGroups.value
|
||||
? new Set()
|
||||
: new Set(taskTypeGroups.value.map(g => g.taskType))
|
||||
: new Set(rowGroups.value.map(g => g.key))
|
||||
}
|
||||
|
||||
watch(groupBy, () => {
|
||||
collapsedGroups.value = new Set()
|
||||
})
|
||||
|
||||
function statusColor(task: TaskListItem): string {
|
||||
const status = taskStatusesStore.getStatusById(props.projectId, task.status)
|
||||
return status?.color || '#94A3B8'
|
||||
@@ -745,6 +783,7 @@ watch(() => props.projectId, () => {
|
||||
collapsedGroups.value = new Set()
|
||||
manualRangeStart.value = ''
|
||||
manualRangeEnd.value = ''
|
||||
groupBy.value = 'taskType'
|
||||
loadTasks()
|
||||
loadSubmissionDates()
|
||||
taskStatusesStore.fetchProjectStatuses(props.projectId)
|
||||
|
||||
Reference in New Issue
Block a user