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( query = db.query(Submission).options(
joinedload(Submission.user), joinedload(Submission.user),
joinedload(Submission.task).joinedload(Task.project), joinedload(Submission.task).joinedload(Task.project),
joinedload(Submission.reviews).joinedload("reviewer") joinedload(Submission.reviews).joinedload(Review.reviewer)
).join(Task).filter( ).join(Task).filter(
Submission.deleted_at.is_(None), Submission.deleted_at.is_(None),
Task.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.""" """Get activities excluding those related to deleted records."""
from sqlalchemy import and_, or_, desc from sqlalchemy import and_, or_, desc
from datetime import datetime, timedelta from datetime import datetime, timedelta
from models.task import Task from models.task import Task, Submission
from models.shot import Shot from models.shot import Shot
from models.asset import Asset from models.asset import Asset
from models.submission import Submission
query = db.query(Activity) 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>