Init Repo
This commit is contained in:
@@ -0,0 +1,480 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-4">
|
||||
<!-- Filters Row -->
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<!-- My Tasks Quick Filter -->
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:class="{'bg-primary text-primary-foreground': myTasksFilter}"
|
||||
@click="toggleMyTasksFilter"
|
||||
class="h-8"
|
||||
>
|
||||
<User class="mr-2 h-4 w-4" />
|
||||
My Tasks
|
||||
</Button>
|
||||
|
||||
<!-- Context Filter Toggle -->
|
||||
<div class="flex items-center border rounded-md h-8 p-0.5">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
:class="{'bg-muted':contextFilter === 'all'}"
|
||||
@click="$emit('update:context-filter', 'all')"
|
||||
class="h-7 px-2"
|
||||
>
|
||||
All
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
:class="{'bg-muted':contextFilter === 'shots'}"
|
||||
@click="$emit('update:context-filter', 'shots')"
|
||||
class="h-7 px-2"
|
||||
>
|
||||
<Film class="h-4 w-4 mr-1" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
:class="{'bg-muted': contextFilter === 'assets'}"
|
||||
@click="$emit('update:context-filter', 'assets')"
|
||||
class="h-7 px-2"
|
||||
>
|
||||
<Package class="h-4 w-4 mr-1" />
|
||||
</Button>
|
||||
</div>
|
||||
<!-- Status Filter -->
|
||||
<Popover>
|
||||
<PopoverTrigger as-child>
|
||||
<Button variant="outline" size="sm" class="h-8 border-dashed">
|
||||
<ListFilter class="mr-2 h-4 w-4" />
|
||||
Status
|
||||
<Badge
|
||||
v-if="statusFilter.length > 0"
|
||||
variant="secondary"
|
||||
class="ml-2 rounded-sm px-1 font-normal"
|
||||
>
|
||||
{{ statusFilter.length }}
|
||||
</Badge>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-[200px] p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search status..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No status found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
v-for="status in statusOptions"
|
||||
:key="status.value"
|
||||
:value="status.value"
|
||||
@select="toggleStatusFilter(status.value)"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
|
||||
statusFilter.includes(status.value)
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'opacity-50 [&_svg]:invisible'
|
||||
]"
|
||||
>
|
||||
<Check class="h-4 w-4" />
|
||||
</div>
|
||||
<span>{{ status.label }}</span>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<!-- Task Type Filter -->
|
||||
<Popover>
|
||||
<PopoverTrigger as-child>
|
||||
<Button variant="outline" size="sm" class="h-8 border-dashed">
|
||||
<Tag class="mr-2 h-4 w-4" />
|
||||
Type
|
||||
<Badge
|
||||
v-if="typeFilter.length > 0"
|
||||
variant="secondary"
|
||||
class="ml-2 rounded-sm px-1 font-normal"
|
||||
>
|
||||
{{ typeFilter.length }}
|
||||
</Badge>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-[200px] p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search type..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No type found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
v-for="type in taskTypes"
|
||||
:key="type"
|
||||
:value="type"
|
||||
@select="toggleTypeFilter(type)"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
|
||||
typeFilter.includes(type)
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'opacity-50 [&_svg]:invisible'
|
||||
]"
|
||||
>
|
||||
<Check class="h-4 w-4" />
|
||||
</div>
|
||||
<span class="capitalize">{{ type.replace(/_/g, ' ') }}</span>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<!-- Episode Filter -->
|
||||
<Popover v-if="episodes.length > 0">
|
||||
<PopoverTrigger as-child>
|
||||
<Button variant="outline" size="sm" class="h-8 border-dashed">
|
||||
<Film class="mr-2 h-4 w-4" />
|
||||
Episode
|
||||
<Badge
|
||||
v-if="episodeFilter !== null"
|
||||
variant="secondary"
|
||||
class="ml-2 rounded-sm px-1 font-normal"
|
||||
>
|
||||
1
|
||||
</Badge>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-[200px] p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search episode..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No episode found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
value="all"
|
||||
@select="$emit('update:episode-filter', null)"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
|
||||
episodeFilter === null
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'opacity-50 [&_svg]:invisible'
|
||||
]"
|
||||
>
|
||||
<Check class="h-4 w-4" />
|
||||
</div>
|
||||
<span>All Episodes</span>
|
||||
</CommandItem>
|
||||
<CommandItem
|
||||
v-for="episode in episodes"
|
||||
:key="episode.id"
|
||||
:value="episode.id.toString()"
|
||||
@select="$emit('update:episode-filter', episode.id)"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
|
||||
episodeFilter === episode.id
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'opacity-50 [&_svg]:invisible'
|
||||
]"
|
||||
>
|
||||
<Check class="h-4 w-4" />
|
||||
</div>
|
||||
<span>{{ episode.name }}</span>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<!-- Assignee Filter -->
|
||||
<Popover v-if="assignees.length > 0">
|
||||
<PopoverTrigger as-child>
|
||||
<Button variant="outline" size="sm" class="h-8 border-dashed">
|
||||
<User class="mr-2 h-4 w-4" />
|
||||
Assignee
|
||||
<Badge
|
||||
v-if="assigneeFilter.length > 0"
|
||||
variant="secondary"
|
||||
class="ml-2 rounded-sm px-1 font-normal"
|
||||
>
|
||||
{{ assigneeFilter.length }}
|
||||
</Badge>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-[200px] p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search assignee..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No assignee found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
v-for="assignee in assignees"
|
||||
:key="assignee.id"
|
||||
:value="assignee.id.toString()"
|
||||
@select="toggleAssigneeFilter(assignee.id)"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
|
||||
assigneeFilter.includes(assignee.id)
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'opacity-50 [&_svg]:invisible'
|
||||
]"
|
||||
>
|
||||
<Check class="h-4 w-4" />
|
||||
</div>
|
||||
<span>{{ assignee.name }}</span>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<!-- Column Visibility -->
|
||||
<Popover>
|
||||
<PopoverTrigger as-child>
|
||||
<Button variant="outline" size="sm" class="ml-auto h-8 border-dashed">
|
||||
<Settings2 class="mr-2 h-4 w-4" />
|
||||
View
|
||||
<Badge
|
||||
v-if="hiddenColumnsCount > 0"
|
||||
variant="secondary"
|
||||
class="ml-2 rounded-sm px-1 font-normal"
|
||||
>
|
||||
{{ hiddenColumnsCount }}
|
||||
</Badge>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-[200px] p-0" align="end">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search columns..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No columns found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
v-for="column in allColumns"
|
||||
:key="column.id"
|
||||
:value="column.id"
|
||||
@select="toggleColumn(column.id, !(columnVisibility[column.id] !== false))"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
|
||||
columnVisibility[column.id] !== false
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'opacity-50 [&_svg]:invisible'
|
||||
]"
|
||||
>
|
||||
<Check class="h-4 w-4" />
|
||||
</div>
|
||||
<span>{{ column.label }}</span>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<!-- Detail Panel Enable/Disable Toggle Button -->
|
||||
<Button
|
||||
@click="$emit('toggle-detail-panel')"
|
||||
:variant="isDetailPanelEnabled ? 'default' : 'outline'"
|
||||
size="sm"
|
||||
:class="[
|
||||
'h-8 w-8 p-0',
|
||||
isDetailPanelEnabled ? 'bg-primary text-primary-foreground hover:bg-primary/90' : ''
|
||||
]"
|
||||
:title="isDetailPanelEnabled ? 'Disable Auto Detail Panel' : 'Enable Auto Detail Panel'"
|
||||
>
|
||||
<PanelRightClose v-if="isDetailPanelEnabled" class="h-4 w-4" />
|
||||
<PanelRightOpen v-else class="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<!-- Search -->
|
||||
<div class="relative flex-1">
|
||||
<Search class="absolute left-2 top-2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
:model-value="search"
|
||||
@update:model-value="debouncedSearch"
|
||||
placeholder="Search tasks..."
|
||||
class="h-8 pl-8"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Clear Filters -->
|
||||
<Button
|
||||
v-if="hasFilters"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-8 px-2 lg:px-3"
|
||||
@click="clearFilters"
|
||||
>
|
||||
Reset
|
||||
<X class="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { Search, ListFilter, Tag, Film, Package, User, Settings2, Check, X, PanelRightClose, PanelRightOpen } from 'lucide-vue-next'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover'
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from '@/components/ui/command'
|
||||
import type { VisibilityState } from '@tanstack/vue-table'
|
||||
import type { Episode } from '@/services/episode'
|
||||
|
||||
interface Props {
|
||||
statusFilter: string[]
|
||||
typeFilter: string[]
|
||||
episodeFilter: number | null
|
||||
assigneeFilter: number[]
|
||||
contextFilter: 'all' | 'shots' | 'assets'
|
||||
search: string
|
||||
columnVisibility: VisibilityState
|
||||
episodes: Episode[]
|
||||
assignees: Array<{ id: number; name: string }>
|
||||
taskTypes: string[]
|
||||
myTasksFilter: boolean
|
||||
currentUserId: number | null
|
||||
isDetailPanelEnabled: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:status-filter': [value: string[]]
|
||||
'update:type-filter': [value: string[]]
|
||||
'update:episode-filter': [value: number | null]
|
||||
'update:assignee-filter': [value: number[]]
|
||||
'update:context-filter': [value: 'all' | 'shots' | 'assets']
|
||||
'update:search': [value: string]
|
||||
'update:column-visibility': [value: VisibilityState]
|
||||
'update:my-tasks-filter': [value: boolean]
|
||||
'toggle-detail-panel': []
|
||||
}>()
|
||||
|
||||
// Status options
|
||||
const statusOptions = [
|
||||
{ value: 'not_started', label: 'Not Started' },
|
||||
{ value: 'in_progress', label: 'In Progress' },
|
||||
{ value: 'submitted', label: 'Submitted' },
|
||||
{ value: 'approved', label: 'Approved' },
|
||||
{ value: 'retake', label: 'Retake' },
|
||||
]
|
||||
|
||||
// Column definitions
|
||||
const allColumns = [
|
||||
{ id: 'name', label: 'Task Name' },
|
||||
{ id: 'task_type', label: 'Type' },
|
||||
{ id: 'status', label: 'Status' },
|
||||
{ id: 'context', label: 'Context' },
|
||||
{ id: 'shot_asset', label: 'Shot/Asset' },
|
||||
{ id: 'episode_name', label: 'Episode' },
|
||||
{ id: 'assigned_user_name', label: 'Assignee' },
|
||||
{ id: 'deadline', label: 'Deadline' },
|
||||
{ id: 'created_at', label: 'Created' },
|
||||
{ id: 'actions', label: 'Actions' },
|
||||
]
|
||||
|
||||
// Computed
|
||||
const hasFilters = computed(() => {
|
||||
return (
|
||||
props.statusFilter.length > 0 ||
|
||||
props.typeFilter.length > 0 ||
|
||||
props.episodeFilter !== null ||
|
||||
props.assigneeFilter.length > 0 ||
|
||||
props.contextFilter !== 'all' ||
|
||||
props.search !== '' ||
|
||||
props.myTasksFilter
|
||||
)
|
||||
})
|
||||
|
||||
const hiddenColumnsCount = computed(() => {
|
||||
return allColumns.filter(col => props.columnVisibility[col.id] === false).length
|
||||
})
|
||||
|
||||
// Debounced search
|
||||
let searchTimeout: ReturnType<typeof setTimeout> | null = null
|
||||
const debouncedSearch = (value: string | number) => {
|
||||
const searchValue = typeof value === 'string' ? value : String(value)
|
||||
if (searchTimeout) clearTimeout(searchTimeout)
|
||||
searchTimeout = setTimeout(() => {
|
||||
emit('update:search', searchValue)
|
||||
}, 300)
|
||||
}
|
||||
|
||||
// Methods
|
||||
const toggleStatusFilter = (status: string) => {
|
||||
const newFilter = props.statusFilter.includes(status)
|
||||
? props.statusFilter.filter((s) => s !== status)
|
||||
: [...props.statusFilter, status]
|
||||
emit('update:status-filter', newFilter)
|
||||
}
|
||||
|
||||
const toggleTypeFilter = (type: string) => {
|
||||
const newFilter = props.typeFilter.includes(type)
|
||||
? props.typeFilter.filter((t) => t !== type)
|
||||
: [...props.typeFilter, type]
|
||||
emit('update:type-filter', newFilter)
|
||||
}
|
||||
|
||||
const toggleAssigneeFilter = (assigneeId: number) => {
|
||||
const newFilter = props.assigneeFilter.includes(assigneeId)
|
||||
? props.assigneeFilter.filter((id) => id !== assigneeId)
|
||||
: [...props.assigneeFilter, assigneeId]
|
||||
emit('update:assignee-filter', newFilter)
|
||||
}
|
||||
|
||||
const toggleColumn = (columnId: string, value: any) => {
|
||||
const newVisibility = { ...props.columnVisibility, [columnId]: value as boolean }
|
||||
emit('update:column-visibility', newVisibility)
|
||||
}
|
||||
|
||||
const toggleMyTasksFilter = () => {
|
||||
const newValue = !props.myTasksFilter
|
||||
emit('update:my-tasks-filter', newValue)
|
||||
|
||||
// When enabling My Tasks, set the assignee filter to current user
|
||||
if (newValue && props.currentUserId) {
|
||||
emit('update:assignee-filter', [props.currentUserId])
|
||||
} else if (!newValue) {
|
||||
// When disabling, clear the assignee filter
|
||||
emit('update:assignee-filter', [])
|
||||
}
|
||||
}
|
||||
|
||||
const clearFilters = () => {
|
||||
emit('update:status-filter', [])
|
||||
emit('update:type-filter', [])
|
||||
emit('update:episode-filter', null)
|
||||
emit('update:assignee-filter', [])
|
||||
emit('update:context-filter', 'all')
|
||||
emit('update:search', '')
|
||||
emit('update:my-tasks-filter', false)
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user