Init Repo
This commit is contained in:
@@ -0,0 +1,295 @@
|
||||
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: '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')))
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user