313 lines
9.4 KiB
TypeScript
313 lines
9.4 KiB
TypeScript
import { h, ref } from 'vue'
|
|
import type { ColumnDef } from '@tanstack/vue-table'
|
|
import { ArrowUpDown, Film, Package, ChevronDown } from 'lucide-vue-next'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Badge } from '@/components/ui/badge'
|
|
import { Checkbox } from '@/components/ui/checkbox'
|
|
import {
|
|
Popover,
|
|
PopoverContent,
|
|
PopoverTrigger,
|
|
} from '@/components/ui/popover'
|
|
import TaskStatusBadge from '@/components/asset/TaskStatusBadge.vue'
|
|
import EditableTaskStatus from '@/components/task/EditableTaskStatus.vue'
|
|
import { type Task } from '@/services/task'
|
|
import { TaskStatus } from '@/services/asset'
|
|
|
|
function formatDate(dateString: string): string {
|
|
const date = new Date(dateString)
|
|
return date.toLocaleDateString('en-US', {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric',
|
|
})
|
|
}
|
|
|
|
interface ColumnCallbacks {
|
|
onBulkStatusChange?: (status: TaskStatus) => void
|
|
onStatusUpdated?: (taskId: number, newStatus: TaskStatus) => void
|
|
getSelectedCount?: () => number
|
|
}
|
|
|
|
export const createColumns = (callbacks?: ColumnCallbacks): ColumnDef<Task>[] => {
|
|
// Create ref outside column definitions so it persists across renders
|
|
const isPopoverOpen = ref(false)
|
|
|
|
return [
|
|
// Select column
|
|
{
|
|
id: 'select',
|
|
header: ({ table }) =>
|
|
h(Checkbox, {
|
|
modelValue: table.getIsAllPageRowsSelected(),
|
|
'onUpdate:modelValue': (value: boolean | 'indeterminate') => table.toggleAllPageRowsSelected(value === true),
|
|
ariaLabel: 'Select all',
|
|
}),
|
|
cell: ({ row }) =>
|
|
h(Checkbox, {
|
|
modelValue: row.getIsSelected(),
|
|
'onUpdate:modelValue': (value: boolean | 'indeterminate') => row.toggleSelected(value === true),
|
|
ariaLabel: 'Select row',
|
|
onClick: (e: Event) => e.stopPropagation(),
|
|
}),
|
|
enableSorting: false,
|
|
enableHiding: false,
|
|
size: 40,
|
|
},
|
|
{
|
|
accessorKey: 'name',
|
|
header: ({ column }) => {
|
|
return h(
|
|
Button,
|
|
{
|
|
variant: 'ghost',
|
|
onClick: () => column.toggleSorting(column.getIsSorted() === 'asc'),
|
|
},
|
|
() => ['Task Name', h(ArrowUpDown, { class: 'ml-2 h-4 w-4' })]
|
|
)
|
|
},
|
|
cell: ({ row }) => {
|
|
return h('div', { class: 'font-medium' }, row.getValue('name'))
|
|
},
|
|
},
|
|
{
|
|
accessorKey: 'project_name',
|
|
header: ({ column }) => {
|
|
return h(
|
|
Button,
|
|
{
|
|
variant: 'ghost',
|
|
onClick: () => column.toggleSorting(column.getIsSorted() === 'asc'),
|
|
},
|
|
() => ['Project', h(ArrowUpDown, { class: 'ml-2 h-4 w-4' })]
|
|
)
|
|
},
|
|
cell: ({ row }) => {
|
|
const projectName = row.getValue('project_name') as string | undefined
|
|
return h('div', { class: 'text-sm' }, projectName || '-')
|
|
},
|
|
},
|
|
{
|
|
accessorKey: 'task_type',
|
|
header: ({ column }) => {
|
|
return h(
|
|
Button,
|
|
{
|
|
variant: 'ghost',
|
|
onClick: () => column.toggleSorting(column.getIsSorted() === 'asc'),
|
|
},
|
|
() => ['Type', h(ArrowUpDown, { class: 'ml-2 h-4 w-4' })]
|
|
)
|
|
},
|
|
cell: ({ row }) => {
|
|
const taskType = row.getValue('task_type') as string
|
|
return h(
|
|
Badge,
|
|
{ variant: 'outline', class: 'capitalize' },
|
|
() => taskType.replace(/_/g, ' ')
|
|
)
|
|
},
|
|
},
|
|
{
|
|
accessorKey: 'status',
|
|
header: ({ column }) => {
|
|
const selectedCount = callbacks?.getSelectedCount?.() || 0
|
|
|
|
if (selectedCount > 0) {
|
|
return h('div', { class: 'flex items-center gap-2' }, [
|
|
h(
|
|
Button,
|
|
{
|
|
variant: 'ghost',
|
|
size: 'sm',
|
|
onClick: () => column.toggleSorting(column.getIsSorted() === 'asc'),
|
|
},
|
|
() => ['Status', h(ArrowUpDown, { class: 'ml-2 h-4 w-4' })]
|
|
),
|
|
h('div', { onClick: (e: Event) => e.stopPropagation() }, [
|
|
h(Popover, {
|
|
open: isPopoverOpen.value,
|
|
'onUpdate:open': (value: boolean) => { isPopoverOpen.value = value }
|
|
}, {
|
|
default: () => [
|
|
h(PopoverTrigger, {}, {
|
|
default: () => h(
|
|
Button,
|
|
{
|
|
variant: 'outline',
|
|
size: 'sm',
|
|
class: 'h-8 w-8 p-0',
|
|
},
|
|
() => h(ChevronDown, { class: 'h-4 w-4' })
|
|
),
|
|
}),
|
|
h(PopoverContent, { class: 'w-48 p-2', align: 'start' }, {
|
|
default: () => {
|
|
|
|
return h('div', { class: 'flex flex-col gap-1' }, [
|
|
h('div', { class: 'px-2 py-1.5 text-sm font-semibold' }, `Change Status`),
|
|
...Object.values(TaskStatus).map((status) =>
|
|
h(
|
|
Button,
|
|
{
|
|
variant: 'ghost',
|
|
size: 'sm',
|
|
class: 'justify-start',
|
|
onClick: () => {
|
|
callbacks?.onBulkStatusChange?.(status)
|
|
isPopoverOpen.value = false
|
|
},
|
|
},
|
|
() => h(TaskStatusBadge, { status, class: 'w-full' })
|
|
)
|
|
),
|
|
])
|
|
},
|
|
}),
|
|
],
|
|
}),
|
|
]),
|
|
])
|
|
}
|
|
|
|
return h(
|
|
Button,
|
|
{
|
|
variant: 'ghost',
|
|
onClick: () => column.toggleSorting(column.getIsSorted() === 'asc'),
|
|
},
|
|
() => ['Status', h(ArrowUpDown, { class: 'ml-2 h-4 w-4' })]
|
|
)
|
|
},
|
|
cell: ({ row }) => {
|
|
const task = row.original
|
|
return h(EditableTaskStatus, {
|
|
taskId: task.id,
|
|
status: row.getValue('status') as TaskStatus,
|
|
projectId: task.project_id,
|
|
onStatusUpdated: (taskId: number, newStatus: TaskStatus) => {
|
|
callbacks?.onStatusUpdated?.(taskId, newStatus)
|
|
},
|
|
})
|
|
},
|
|
},
|
|
{
|
|
id: 'context',
|
|
header: 'Context',
|
|
cell: ({ row }) => {
|
|
const task = row.original
|
|
const isShot = !!task.shot_id
|
|
const icon = isShot ? Film : Package
|
|
const label = isShot ? 'Shot' : 'Asset'
|
|
|
|
return h('div', { class: 'flex items-center gap-2' }, [
|
|
h(icon, { class: 'h-4 w-4 text-muted-foreground' }),
|
|
h('span', { class: 'text-sm text-muted-foreground' }, label),
|
|
])
|
|
},
|
|
},
|
|
{
|
|
id: 'shot_asset',
|
|
header: ({ column }) => {
|
|
return h(
|
|
Button,
|
|
{
|
|
variant: 'ghost',
|
|
onClick: () => column.toggleSorting(column.getIsSorted() === 'asc'),
|
|
},
|
|
() => ['Shot/Asset', h(ArrowUpDown, { class: 'ml-2 h-4 w-4' })]
|
|
)
|
|
},
|
|
accessorFn: (row) => row.shot_name || row.asset_name || '',
|
|
cell: ({ row }) => {
|
|
const task = row.original
|
|
const name = task.shot_name || task.asset_name || '-'
|
|
return h('div', { class: 'font-medium' }, name)
|
|
},
|
|
},
|
|
{
|
|
accessorKey: 'episode_name',
|
|
header: ({ column }) => {
|
|
return h(
|
|
Button,
|
|
{
|
|
variant: 'ghost',
|
|
onClick: () => column.toggleSorting(column.getIsSorted() === 'asc'),
|
|
},
|
|
() => ['Episode', h(ArrowUpDown, { class: 'ml-2 h-4 w-4' })]
|
|
)
|
|
},
|
|
cell: ({ row }) => {
|
|
const episodeName = row.getValue('episode_name') as string | undefined
|
|
return h('div', { class: 'text-sm' }, episodeName || '-')
|
|
},
|
|
},
|
|
{
|
|
accessorKey: 'assigned_user_name',
|
|
header: ({ column }) => {
|
|
return h(
|
|
Button,
|
|
{
|
|
variant: 'ghost',
|
|
onClick: () => column.toggleSorting(column.getIsSorted() === 'asc'),
|
|
},
|
|
() => ['Assignee', h(ArrowUpDown, { class: 'ml-2 h-4 w-4' })]
|
|
)
|
|
},
|
|
cell: ({ row }) => {
|
|
const assigneeName = row.getValue('assigned_user_name') as string | undefined
|
|
return h('div', { class: 'text-sm' }, assigneeName || 'Unassigned')
|
|
},
|
|
},
|
|
{
|
|
accessorKey: 'deadline',
|
|
header: ({ column }) => {
|
|
return h(
|
|
Button,
|
|
{
|
|
variant: 'ghost',
|
|
onClick: () => column.toggleSorting(column.getIsSorted() === 'asc'),
|
|
},
|
|
() => ['Deadline', h(ArrowUpDown, { class: 'ml-2 h-4 w-4' })]
|
|
)
|
|
},
|
|
cell: ({ row }) => {
|
|
const deadline = row.getValue('deadline') as string | undefined
|
|
if (!deadline) return h('div', { class: 'text-sm text-muted-foreground' }, '-')
|
|
|
|
const date = new Date(deadline)
|
|
const now = new Date()
|
|
const isOverdue = date < now
|
|
const isUrgent = date.getTime() - now.getTime() < 3 * 24 * 60 * 60 * 1000 // 3 days
|
|
|
|
return h(
|
|
'div',
|
|
{
|
|
class: [
|
|
'text-sm',
|
|
isOverdue ? 'text-destructive font-medium' : isUrgent ? 'text-orange-600 font-medium' : '',
|
|
],
|
|
},
|
|
formatDate(deadline)
|
|
)
|
|
},
|
|
},
|
|
{
|
|
accessorKey: 'created_at',
|
|
header: ({ column }) => {
|
|
return h(
|
|
Button,
|
|
{
|
|
variant: 'ghost',
|
|
onClick: () => column.toggleSorting(column.getIsSorted() === 'asc'),
|
|
},
|
|
() => ['Created', h(ArrowUpDown, { class: 'ml-2 h-4 w-4' })]
|
|
)
|
|
},
|
|
cell: ({ row }) => {
|
|
return h('div', { class: 'text-sm text-muted-foreground' }, formatDate(row.getValue('created_at')))
|
|
},
|
|
},
|
|
]
|
|
}
|