LinkDesk/frontend/src/components/task/columns.ts

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')))
},
},
]
}