Fix broken joinedload and import path in reviews/activities endpoints

joinedload("reviewer") used a string instead of a class-bound
attribute (rejected by SQLAlchemy 2.x), and models.submission was the
wrong import path for Submission. Both endpoints 500'd whenever
actually called; surfaced while wiring real dashboard/activity data
into the frontend.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 04:46:43 +08:00
parent 61ee1a7364
commit c63a71883b
9 changed files with 2 additions and 1074 deletions
+1 -1
View File
@@ -66,7 +66,7 @@ async def get_pending_reviews(
query = db.query(Submission).options(
joinedload(Submission.user),
joinedload(Submission.task).joinedload(Task.project),
joinedload(Submission.reviews).joinedload("reviewer")
joinedload(Submission.reviews).joinedload(Review.reviewer)
).join(Task).filter(
Submission.deleted_at.is_(None),
Task.deleted_at.is_(None)
+1 -2
View File
@@ -302,10 +302,9 @@ class ActivityService:
"""Get activities excluding those related to deleted records."""
from sqlalchemy import and_, or_, desc
from datetime import datetime, timedelta
from models.task import Task
from models.task import Task, Submission
from models.shot import Shot
from models.asset import Asset
from models.submission import Submission
query = db.query(Activity)
@@ -1,209 +0,0 @@
<template>
<div class="space-y-4">
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold">Activity Timeline</h3>
<Button
variant="ghost"
size="icon"
@click="loadActivities"
:disabled="loading"
>
<RefreshCw class="h-4 w-4" :class="{ 'animate-spin': loading }" />
</Button>
</div>
<div v-if="loading && activities.length === 0" class="text-center py-8 text-muted-foreground">
Loading timeline...
</div>
<div v-else-if="activities.length === 0" class="text-center py-8 text-muted-foreground">
<Clock class="h-12 w-12 mx-auto mb-2 opacity-50" />
<p>No activity recorded</p>
</div>
<div v-else class="relative">
<!-- Timeline line -->
<div class="absolute left-4 top-0 bottom-0 w-0.5 bg-border" />
<!-- Timeline items -->
<div class="space-y-6">
<div
v-for="activity in activities"
:key="activity.id"
class="relative pl-10"
>
<!-- Timeline dot -->
<div
class="absolute left-2.5 w-3 h-3 rounded-full border-2 border-background"
:class="getTimelineDotColor(activity.type)"
/>
<!-- Activity content -->
<div class="bg-card border rounded-lg p-4">
<div class="flex items-start gap-3">
<component
:is="getActivityIcon(activity.type)"
class="h-5 w-5 mt-0.5 flex-shrink-0"
:class="getActivityColor(activity.type)"
/>
<div class="flex-1 min-w-0">
<p class="text-sm font-medium">{{ getActivityTitle(activity.type) }}</p>
<p class="text-sm text-muted-foreground mt-1">{{ activity.description }}</p>
<!-- Metadata display -->
<div v-if="activity.activity_metadata" class="mt-2 text-xs text-muted-foreground">
<div v-if="activity.activity_metadata.old_status && activity.activity_metadata.new_status">
<Badge variant="outline" class="mr-2">{{ activity.activity_metadata.old_status }}</Badge>
<Badge variant="outline" class="ml-2">{{ activity.activity_metadata.new_status }}</Badge>
</div>
<div v-if="activity.activity_metadata.version">
Version {{ activity.activity_metadata.version }}
</div>
<div v-if="activity.activity_metadata.decision">
Decision: <Badge :variant="activity.activity_metadata.decision === 'approved' ? 'default' : 'destructive'">
{{ activity.activity_metadata.decision }}
</Badge>
</div>
</div>
<div class="flex items-center gap-2 mt-2">
<Avatar class="h-5 w-5">
<AvatarFallback class="text-xs">
{{ getInitials(activity.user) }}
</AvatarFallback>
</Avatar>
<span class="text-xs text-muted-foreground">
{{ activity.user.first_name }} {{ activity.user.last_name }}
</span>
<span class="text-xs text-muted-foreground"></span>
<span class="text-xs text-muted-foreground">
{{ formatTime(activity.created_at) }}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue'
import { Button } from '@/components/ui/button'
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
import { Badge } from '@/components/ui/badge'
import {
Clock,
FileText,
CheckCircle,
UserPlus,
MessageSquare,
RefreshCw
} from 'lucide-vue-next'
import type { Activity, ActivityType, UserInfo } from '@/types/activity'
import * as activityService from '@/services/activity'
interface Props {
taskId: number
}
const props = defineProps<Props>()
const activities = ref<Activity[]>([])
const loading = ref(false)
onMounted(() => {
loadActivities()
})
watch(() => props.taskId, () => {
loadActivities()
})
async function loadActivities() {
loading.value = true
try {
activities.value = await activityService.getTaskActivities(props.taskId)
} catch (error) {
console.error('Failed to load task activities:', error)
} finally {
loading.value = false
}
}
function getInitials(user: UserInfo): string {
return `${user.first_name[0]}${user.last_name[0]}`.toUpperCase()
}
function getActivityIcon(type: ActivityType) {
const iconMap: Record<string, any> = {
task_created: FileText,
task_updated: FileText,
task_assigned: UserPlus,
task_status_changed: CheckCircle,
submission_created: FileText,
submission_reviewed: CheckCircle,
comment_added: MessageSquare
}
return iconMap[type] || FileText
}
function getActivityColor(type: ActivityType): string {
const colorMap: Record<string, string> = {
task_created: 'text-blue-500',
task_updated: 'text-blue-500',
task_assigned: 'text-purple-500',
task_status_changed: 'text-green-500',
submission_created: 'text-orange-500',
submission_reviewed: 'text-green-500',
comment_added: 'text-gray-500'
}
return colorMap[type] || 'text-gray-500'
}
function getTimelineDotColor(type: ActivityType): string {
const colorMap: Record<string, string> = {
task_created: 'bg-blue-500',
task_updated: 'bg-blue-500',
task_assigned: 'bg-purple-500',
task_status_changed: 'bg-green-500',
submission_created: 'bg-orange-500',
submission_reviewed: 'bg-green-500',
comment_added: 'bg-gray-500'
}
return colorMap[type] || 'bg-gray-500'
}
function getActivityTitle(type: ActivityType): string {
const titleMap: Record<string, string> = {
task_created: 'Task Created',
task_updated: 'Task Updated',
task_assigned: 'Task Assigned',
task_status_changed: 'Status Changed',
submission_created: 'Work Submitted',
submission_reviewed: 'Submission Reviewed',
comment_added: 'Comment Added'
}
return titleMap[type] || 'Activity'
}
function formatTime(timestamp: string): string {
const date = new Date(timestamp)
const now = new Date()
const diffMs = now.getTime() - date.getTime()
const diffMins = Math.floor(diffMs / 60000)
const diffHours = Math.floor(diffMs / 3600000)
const diffDays = Math.floor(diffMs / 86400000)
if (diffMins < 1) return 'Just now'
if (diffMins < 60) return `${diffMins}m ago`
if (diffHours < 24) return `${diffHours}h ago`
if (diffDays < 7) return `${diffDays}d ago`
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString()
}
</script>
@@ -1,256 +0,0 @@
<template>
<div class="flex items-center gap-2">
<!-- Global Task Columns Toggle Button -->
<Button
variant="outline"
size="sm"
@click="toggleAllTaskColumns"
class="h-9"
>
<ListTodo v-if="!allTaskColumnsVisible" class="h-4 w-4 mr-2" />
<ListX v-else class="h-4 w-4 mr-2" />
{{ allTaskColumnsVisible ? 'Hide' : 'Show' }} Tasks
</Button>
<!-- Column Visibility Dropdown -->
<Select v-model="selectedColumn" @update:model-value="handleColumnToggle">
<SelectTrigger class="w-[180px]">
<SelectValue placeholder="Toggle columns">
<div class="flex items-center gap-2">
<Columns class="h-4 w-4" />
<span>Columns</span>
</div>
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="toggle">Toggle Columns</SelectItem>
<SelectGroup>
<SelectLabel>Basic Columns</SelectLabel>
<SelectItem value="thumbnail" @click.stop>
<div class="flex items-center gap-2">
<Checkbox
v-model="visibleColumns.thumbnail"
@update:checked="(val: boolean | 'indeterminate') => updateColumn('thumbnail', val)"
/>
<span>Thumbnail</span>
</div>
</SelectItem>
<SelectItem value="name" @click.stop>
<div class="flex items-center gap-2">
<Checkbox
v-model="visibleColumns.name"
@update:checked="(val: boolean | 'indeterminate') => updateColumn('name', val)"
/>
<span>Name</span>
</div>
</SelectItem>
<SelectItem value="category" @click.stop>
<div class="flex items-center gap-2">
<Checkbox
v-model="visibleColumns.category"
@update:checked="(val: boolean | 'indeterminate') => updateColumn('category', val)"
/>
<span>Category</span>
</div>
</SelectItem>
<SelectItem value="status" @click.stop>
<div class="flex items-center gap-2">
<Checkbox
v-model="visibleColumns.status"
@update:checked="(val: boolean | 'indeterminate') => updateColumn('status', val)"
/>
<span>Status</span>
</div>
</SelectItem>
</SelectGroup>
<SelectGroup>
<SelectLabel>Task Status Columns</SelectLabel>
<!-- Standard Task Types -->
<SelectItem value="modeling" @click.stop>
<div class="flex items-center gap-2">
<Checkbox
v-model="visibleColumns.modeling"
@update:checked="(val: boolean | 'indeterminate') => updateColumn('modeling', val)"
/>
<span>Modeling</span>
</div>
</SelectItem>
<SelectItem value="surfacing" @click.stop>
<div class="flex items-center gap-2">
<Checkbox
v-model="visibleColumns.surfacing"
@update:checked="(val: boolean | 'indeterminate') => updateColumn('surfacing', val)"
/>
<span>Surfacing</span>
</div>
</SelectItem>
<SelectItem value="rigging" @click.stop>
<div class="flex items-center gap-2">
<Checkbox
v-model="visibleColumns.rigging"
@update:checked="(val: boolean | 'indeterminate') => updateColumn('rigging', val)"
/>
<span>Rigging</span>
</div>
</SelectItem>
<!-- Custom Task Types -->
<SelectItem
v-for="customType in customTaskTypes"
:key="customType"
:value="customType"
@click.stop
>
<div class="flex items-center gap-2">
<Checkbox
v-model="visibleColumns[customType]"
@update:checked="(val: boolean | 'indeterminate') => updateColumn(customType, val)"
/>
<span>{{ formatTaskType(customType) }}</span>
</div>
</SelectItem>
</SelectGroup>
<SelectGroup>
<SelectLabel>Other Columns</SelectLabel>
<SelectItem value="description" @click.stop>
<div class="flex items-center gap-2">
<Checkbox
v-model="visibleColumns.description"
@update:checked="(val: boolean | 'indeterminate') => updateColumn('description', val)"
/>
<span>Description</span>
</div>
</SelectItem>
<SelectItem value="updatedAt" @click.stop>
<div class="flex items-center gap-2">
<Checkbox
v-model="visibleColumns.updatedAt"
@update:checked="(val: boolean | 'indeterminate') => updateColumn('updatedAt', val)"
/>
<span>Updated</span>
</div>
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { Columns, ListTodo, ListX } from 'lucide-vue-next'
import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
interface Props {
visibleColumns: Record<string, boolean>;
projectId?: number;
}
interface Emits {
(e: "update:visibleColumns", columns: Record<string, boolean>): void;
}
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
const selectedColumn = ref('toggle')
const customTaskTypes = ref<string[]>([])
const savedTaskColumnStates = ref<Record<string, boolean>>({})
// Standard task types
const standardTaskTypes = ['modeling', 'surfacing', 'rigging']
// All task types (standard + custom)
const allTaskTypes = computed(() => [...standardTaskTypes, ...customTaskTypes.value])
// Check if all task columns are visible
const allTaskColumnsVisible = computed(() => {
return allTaskTypes.value.every(taskType => props.visibleColumns[taskType])
})
// Load custom task types from project
const loadCustomTaskTypes = async () => {
if (!props.projectId) return
try {
const { projectService } = await import('@/services/project')
const project = await projectService.getProject(props.projectId)
customTaskTypes.value = project.custom_asset_task_types || []
// Initialize visibility for custom task types if not already set
const newColumns = { ...props.visibleColumns }
let hasChanges = false
for (const customType of customTaskTypes.value) {
if (!(customType in newColumns)) {
newColumns[customType] = true // Show custom types by default
hasChanges = true
}
}
if (hasChanges) {
emit("update:visibleColumns", newColumns)
}
} catch (error) {
console.warn('Could not load custom task types:', error)
// Continue without custom task types
}
}
// Toggle all task columns show/hide
const toggleAllTaskColumns = () => {
const newColumns = { ...props.visibleColumns }
if (allTaskColumnsVisible.value) {
// Hide all task columns but save their states
savedTaskColumnStates.value = {}
for (const taskType of allTaskTypes.value) {
savedTaskColumnStates.value[taskType] = newColumns[taskType]
newColumns[taskType] = false
}
} else {
// Restore saved states or show all
for (const taskType of allTaskTypes.value) {
if (taskType in savedTaskColumnStates.value) {
newColumns[taskType] = savedTaskColumnStates.value[taskType]
} else {
newColumns[taskType] = true
}
}
savedTaskColumnStates.value = {}
}
emit("update:visibleColumns", newColumns)
}
const handleColumnToggle = () => {
// Reset selection after interaction
selectedColumn.value = 'toggle'
}
const updateColumn = (column: string, checked: boolean | 'indeterminate') => {
const newColumns = { ...props.visibleColumns }
newColumns[column] = checked === true
emit('update:visibleColumns', newColumns)
}
const formatTaskType = (taskType: string) => {
return taskType
.split('_')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ')
}
onMounted(() => {
loadCustomTaskTypes()
})
</script>
@@ -1,123 +0,0 @@
<template>
<Card>
<CardHeader>
<CardTitle>File Upload Example</CardTitle>
<CardDescription>
Example showing how to integrate upload limit display and validation
</CardDescription>
</CardHeader>
<CardContent class="space-y-4">
<!-- Upload Limit Display -->
<UploadLimitDisplay />
<!-- File Upload -->
<div class="space-y-2">
<Label for="file-upload">Select File</Label>
<Input
id="file-upload"
type="file"
@change="handleFileSelect"
:disabled="uploading"
accept=".mov,.mp4,.avi,.mkv,.webm,.jpg,.jpeg,.png,.exr,.tiff"
/>
</div>
<!-- File Info -->
<div v-if="selectedFile" class="text-sm space-y-1">
<p><strong>File:</strong> {{ selectedFile.name }}</p>
<p><strong>Size:</strong> {{ formatFileSize(selectedFile.size) }}</p>
<p><strong>Type:</strong> {{ getFileType(selectedFile.name) }}</p>
</div>
<!-- Validation Errors -->
<div v-if="validationError" class="text-sm text-destructive">
{{ validationError }}
</div>
<!-- Upload Button -->
<Button
@click="uploadFile"
:disabled="!selectedFile || !!validationError || uploading"
class="w-full"
>
<Loader2 v-if="uploading" class="h-4 w-4 animate-spin mr-2" />
<Upload v-else class="h-4 w-4 mr-2" />
{{ uploading ? 'Uploading...' : 'Upload File' }}
</Button>
<!-- Success Message -->
<div v-if="uploadSuccess" class="text-sm text-green-600">
File uploaded successfully!
</div>
</CardContent>
</Card>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Upload, Loader2 } from 'lucide-vue-next'
import UploadLimitDisplay from '@/components/settings/UploadLimitDisplay.vue'
import { validateFile, formatFileSize, isMovieFile, isImageFile } from '@/utils/fileValidation'
const selectedFile = ref<File | null>(null)
const validationError = ref<string | null>(null)
const uploading = ref(false)
const uploadSuccess = ref(false)
function getFileType(fileName: string): string {
if (isMovieFile(fileName)) return 'Movie'
if (isImageFile(fileName)) return 'Image'
return 'Other'
}
async function handleFileSelect(event: Event) {
const target = event.target as HTMLInputElement
const file = target.files?.[0]
if (!file) {
selectedFile.value = null
validationError.value = null
return
}
selectedFile.value = file
uploadSuccess.value = false
// Validate file
const result = await validateFile(file)
validationError.value = result.isValid ? null : result.error || 'Invalid file'
}
async function uploadFile() {
if (!selectedFile.value || validationError.value) return
uploading.value = true
try {
// Simulate upload delay
await new Promise(resolve => setTimeout(resolve, 2000))
// Here you would make the actual API call to upload the file
// const formData = new FormData()
// formData.append('file', selectedFile.value)
// await api.post('/tasks/123/attachments', formData)
uploadSuccess.value = true
selectedFile.value = null
validationError.value = null
// Reset file input
const fileInput = document.getElementById('file-upload') as HTMLInputElement
if (fileInput) fileInput.value = ''
} catch (error) {
validationError.value = 'Upload failed. Please try again.'
} finally {
uploading.value = false
}
}
</script>
@@ -1,281 +0,0 @@
<template>
<div class="space-y-4">
<!-- Table Header Actions -->
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<h3 class="text-lg font-semibold">
{{ episodeId ? `Episode ${episodeId} Shots` : "All Shots" }}
</h3>
<Badge variant="secondary" v-if="shots.length > 0">
{{ shots.length }} shot{{ shots.length !== 1 ? "s" : "" }}
</Badge>
</div>
<div class="flex items-center gap-2">
<Button variant="outline" size="sm" @click="refreshShots">
<RefreshCw class="h-4 w-4 mr-2" />
Refresh
</Button>
<Button size="sm" @click="createShot" v-if="episodeId">
<Plus class="h-4 w-4 mr-2" />
Add Shot
</Button>
</div>
</div>
<!-- Loading State -->
<div v-if="isLoading" class="flex items-center justify-center py-8">
<div class="flex items-center gap-2">
<div
class="animate-spin rounded-full h-6 w-6 border-b-2 border-primary"
></div>
<span class="text-muted-foreground">Loading shots...</span>
</div>
</div>
<!-- Error State -->
<div v-else-if="error" class="text-center py-8">
<AlertCircle class="h-8 w-8 mx-auto text-destructive mb-2" />
<p class="text-muted-foreground">{{ error }}</p>
<Button variant="outline" size="sm" @click="refreshShots" class="mt-2">
Try Again
</Button>
</div>
<!-- Empty State -->
<div v-else-if="shots.length === 0" class="text-center py-12">
<Camera class="h-12 w-12 mx-auto mb-4 text-muted-foreground" />
<h3 class="text-lg font-semibold mb-2">No shots found</h3>
<p class="text-muted-foreground mb-4">
{{
episodeId
? "This episode doesn't have any shots yet."
: "No shots found for the selected criteria."
}}
</p>
<Button @click="createShot" v-if="episodeId">
<Plus class="h-4 w-4 mr-2" />
Create First Shot
</Button>
</div>
<!-- Shots Table -->
<div v-else class="border rounded-lg">
<Table>
<TableHeader>
<TableRow>
<TableHead>Shot Name</TableHead>
<TableHead>Description</TableHead>
<TableHead>Frames</TableHead>
<TableHead>Status</TableHead>
<TableHead>Tasks</TableHead>
<TableHead>Updated</TableHead>
<TableHead class="w-[100px]">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow
v-for="shot in shots"
:key="shot.id"
class="hover:bg-muted/50"
>
<TableCell class="font-medium">{{ shot.name }}</TableCell>
<TableCell>
<span
v-if="shot.description"
class="text-sm text-muted-foreground"
>
{{ shot.description }}
</span>
<span v-else class="text-sm text-muted-foreground italic"
>No description</span
>
</TableCell>
<TableCell>
<span class="font-mono text-sm">
{{ shot.frame_start }}-{{ shot.frame_end }}
</span>
<span class="text-xs text-muted-foreground ml-2">
({{ shot.frame_end - shot.frame_start + 1 }} frames)
</span>
</TableCell>
<TableCell>
<Badge :variant="getStatusVariant(shot.status)">
{{ formatStatus(shot.status) }}
</Badge>
</TableCell>
<TableCell>
<span class="text-sm">{{ shot.task_count }} tasks</span>
</TableCell>
<TableCell>
<span class="text-sm text-muted-foreground">
{{ formatDate(shot.updated_at) }}
</span>
</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm">
<MoreHorizontal class="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem @click="editShot(shot)">
<Edit class="h-4 w-4 mr-2" />
Edit
</DropdownMenuItem>
<DropdownMenuItem @click="viewTasks(shot)">
<CheckSquare class="h-4 w-4 mr-2" />
View Tasks
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
@click="deleteShot(shot)"
class="text-destructive"
>
<Trash2 class="h-4 w-4 mr-2" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted } from "vue";
import {
Camera,
Plus,
RefreshCw,
AlertCircle,
MoreHorizontal,
Edit,
CheckSquare,
Trash2,
} from "lucide-vue-next";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { shotService, type Shot, ShotStatus } from "@/services/shot";
interface Props {
projectId: number;
episodeId?: number | null;
}
const props = defineProps<Props>();
// Reactive state
const shots = ref<Shot[]>([]);
const isLoading = ref(false);
const error = ref<string | null>(null);
// Methods
const loadShots = async () => {
if (!props.projectId) return;
try {
isLoading.value = true;
error.value = null;
const shotsData = await shotService.getShots(
props.projectId,
props.episodeId || undefined
);
shots.value = shotsData;
} catch (err) {
error.value = err instanceof Error ? err.message : "Failed to load shots";
shots.value = [];
} finally {
isLoading.value = false;
}
};
const refreshShots = () => {
loadShots();
};
const createShot = () => {
// TODO: Implement shot creation dialog
console.log("Create shot for episode:", props.episodeId);
};
const editShot = (shot: Shot) => {
// TODO: Implement shot editing
console.log("Edit shot:", shot);
};
const viewTasks = (shot: Shot) => {
// TODO: Navigate to shot tasks view
console.log("View tasks for shot:", shot);
};
const deleteShot = async (shot: Shot) => {
// TODO: Implement shot deletion with confirmation
console.log("Delete shot:", shot);
};
const getStatusVariant = (status: ShotStatus) => {
switch (status) {
case ShotStatus.NOT_STARTED:
return "secondary";
case ShotStatus.IN_PROGRESS:
return "default";
case ShotStatus.ON_HOLD:
return "outline";
case ShotStatus.COMPLETED:
return "default";
case ShotStatus.APPROVED:
return "default";
default:
return "secondary";
}
};
const formatStatus = (status: ShotStatus) => {
return status
.split("_")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ");
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
};
// Watchers
watch(
() => [props.projectId, props.episodeId],
() => {
loadShots();
},
{ immediate: true }
);
// Lifecycle
onMounted(() => {
loadShots();
});
</script>
@@ -1,159 +0,0 @@
<template>
<div class="flex items-center gap-2">
<Select v-model="selectedColumn" @update:model-value="handleColumnToggle">
<SelectTrigger class="w-[180px]">
<SelectValue placeholder="Toggle columns">
<div class="flex items-center gap-2">
<Columns class="h-4 w-4" />
<span>Columns</span>
</div>
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="toggle">Toggle Columns</SelectItem>
<SelectGroup>
<SelectLabel>Basic Columns</SelectLabel>
<SelectItem value="thumbnail" @click="toggleColumn('thumbnail')">
<div class="flex items-center gap-2">
<input
type="checkbox"
:checked="isColumnVisible('thumbnail')"
@change="handleCheckboxChange('thumbnail', $event)"
class="rounded border-gray-300"
/>
<span>Thumbnail</span>
</div>
</SelectItem>
<SelectItem value="name" @click="toggleColumn('name')">
<div class="flex items-center gap-2">
<input
type="checkbox"
:checked="isColumnVisible('name')"
@change="handleCheckboxChange('name', $event)"
class="rounded border-gray-300"
/>
<span>Shot Name</span>
</div>
</SelectItem>
<SelectItem value="episode" @click="toggleColumn('episode')">
<div class="flex items-center gap-2">
<input
type="checkbox"
:checked="isColumnVisible('episode')"
@change="handleCheckboxChange('episode', $event)"
class="rounded border-gray-300"
/>
<span>Episode</span>
</div>
</SelectItem>
<SelectItem value="status" @click="toggleColumn('status')">
<div class="flex items-center gap-2">
<input
type="checkbox"
:checked="isColumnVisible('status')"
@change="handleCheckboxChange('status', $event)"
class="rounded border-gray-300"
/>
<span>Status</span>
</div>
</SelectItem>
</SelectGroup>
<SelectGroup>
<SelectLabel>Task Status Columns</SelectLabel>
<SelectItem
v-for="taskType in allTaskTypes"
:key="taskType"
:value="taskType"
@click="toggleColumn(taskType)"
>
<div class="flex items-center gap-2">
<input
type="checkbox"
:checked="isColumnVisible(taskType)"
@change="handleCheckboxChange(taskType, $event)"
class="rounded border-gray-300"
/>
<span>{{ formatTaskType(taskType) }}</span>
</div>
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue'
import { Columns } from 'lucide-vue-next'
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import type { VisibilityState } from '@tanstack/vue-table'
import { useColumnVisibilityStore } from '@/stores/columnVisibility'
interface Props {
allTaskTypes: string[]
}
const props = defineProps<Props>()
const emit = defineEmits<{
'update:columnVisibility': [visibility: VisibilityState]
}>()
// Use global store
const columnVisibilityStore = useColumnVisibilityStore()
// Local reactive state for checkbox visibility
const localVisibility = ref<Record<string, boolean>>({})
// Watch for store changes and update local state
watch(
() => columnVisibilityStore.columnVisibility,
(newVal) => {
localVisibility.value = { ...newVal }
},
{ immediate: true, deep: true }
)
// Initialize store on mount
onMounted(() => {
columnVisibilityStore.initialize()
})
const selectedColumn = ref('toggle')
const handleColumnToggle = (value: string) => {
// Reset selection after interaction
selectedColumn.value = 'toggle'
}
const isColumnVisible = (columnId: string): boolean => {
// If not in visibility state, column is visible by default
return localVisibility.value[columnId] !== false
}
const handleCheckboxChange = (column: string, event: Event) => {
const target = event.target as HTMLInputElement
updateColumn(column, target.checked)
}
const toggleColumn = (column: string) => {
updateColumn(column, !isColumnVisible(column))
}
const updateColumn = (column: string, checked: boolean) => {
// Always use global store
columnVisibilityStore.updateColumn(column, checked)
}
const formatTaskType = (taskType: string) => {
return taskType.charAt(0).toUpperCase() + taskType.slice(1)
}
</script>
-18
View File
@@ -1,18 +0,0 @@
<template>
<div class="container mx-auto py-6 space-y-6">
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold">Global Settings</h1>
<p class="text-muted-foreground">
Manage system-wide configuration settings
</p>
</div>
</div>
<GlobalSettingsPanel />
</div>
</template>
<script setup lang="ts">
import GlobalSettingsPanel from '@/components/settings/GlobalSettingsPanel.vue'
</script>
-25
View File
@@ -1,25 +0,0 @@
<template>
<div class="min-h-screen bg-background">
<div class="container mx-auto px-4 py-8">
<h1 class="text-4xl font-bold text-foreground mb-4">
VFX Project Management System
</h1>
<p class="text-muted-foreground text-lg">
Welcome to the VFX Project Management System. This application will help you manage
animation and VFX production workflows.
</p>
<div class="mt-8">
<router-link
to="/login"
class="inline-flex items-center px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors"
>
Get Started
</router-link>
</div>
</div>
</div>
</template>
<script setup lang="ts">
// Home view component
</script>