Init Repo
This commit is contained in:
@@ -0,0 +1,520 @@
|
||||
<template>
|
||||
<div class="h-full flex flex-col">
|
||||
<!-- Loading State -->
|
||||
<div v-if="isLoading" class="flex items-center justify-center py-12">
|
||||
<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 shot details...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
<div v-else-if="error" class="p-6 text-center">
|
||||
<AlertCircle class="h-12 w-12 mx-auto text-destructive mb-4" />
|
||||
<h3 class="text-lg font-semibold mb-2">Failed to load shot</h3>
|
||||
<p class="text-muted-foreground mb-4">{{ error }}</p>
|
||||
<Button @click="loadShotDetails" variant="outline">
|
||||
<RefreshCw class="h-4 w-4 mr-2" />
|
||||
Try Again
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Shot Details -->
|
||||
<div v-else-if="shot" class="flex-1 overflow-y-auto">
|
||||
<!-- Header -->
|
||||
<div class="p-6 border-b">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-3 min-w-0 flex-1">
|
||||
<h2 class="text-xl font-bold truncate" :class="{ 'line-through text-muted-foreground': shot.deleted_at }">{{ shot.name }}</h2>
|
||||
<Badge :variant="getStatusVariant(shot.status)" class="text-xs flex-shrink-0">
|
||||
<div
|
||||
class="w-2 h-2 rounded-full mr-1"
|
||||
:class="getStatusColor(shot.status)"
|
||||
></div>
|
||||
{{ formatStatus(shot.status) }}
|
||||
</Badge>
|
||||
<!-- Deletion status indicator for admins -->
|
||||
<Badge v-if="authStore.isAdmin && shot.deleted_at" variant="destructive" class="text-xs flex-shrink-0">
|
||||
Deleted {{ formatDeletedDate(shot.deleted_at) }}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<!-- Close Button -->
|
||||
<Button variant="ghost" size="sm" class="h-8 w-8 p-0 flex-shrink-0" @click="$emit('close')">
|
||||
<X class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabbed Content -->
|
||||
<Tabs default-value="infos" class="flex-1 flex flex-col">
|
||||
<TabsList class="mx-0 mt-0 grid w-full grid-cols-5 rounded-none border-b">
|
||||
<TabsTrigger value="infos">Infos</TabsTrigger>
|
||||
<TabsTrigger value="notes">Notes</TabsTrigger>
|
||||
<TabsTrigger value="assets">Assets</TabsTrigger>
|
||||
<TabsTrigger value="references">References</TabsTrigger>
|
||||
<TabsTrigger value="design">Design</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<!-- Infos Tab -->
|
||||
<TabsContent value="infos" class="flex-1 p-6 space-y-6">
|
||||
<!-- Shot Information -->
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-sm font-semibold">Shot Information</h3>
|
||||
<div class="space-y-3">
|
||||
<div class="grid grid-cols-2 gap-4 text-xs">
|
||||
<div>
|
||||
<Label class="text-muted-foreground">Frame Range</Label>
|
||||
<p class="text-sm mt-1">{{ shot.frame_start }} - {{ shot.frame_end }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label class="text-muted-foreground">Duration</Label>
|
||||
<p class="text-sm mt-1">{{ frameCount }} frames</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label class="text-xs text-muted-foreground">Description</Label>
|
||||
<p class="text-sm mt-1">
|
||||
{{ shot.description || 'No description provided' }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4 text-xs">
|
||||
<div>
|
||||
<Label class="text-muted-foreground">Created</Label>
|
||||
<p>{{ formatDate(shot.created_at) }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label class="text-muted-foreground">Updated</Label>
|
||||
<p>{{ formatDate(shot.updated_at) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Progress Overview -->
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-sm font-semibold">Progress Overview</h3>
|
||||
<span class="text-sm text-muted-foreground">
|
||||
{{ completedTasksCount }} / {{ tasks.length }} tasks
|
||||
</span>
|
||||
</div>
|
||||
<!-- Progress Bar -->
|
||||
<div class="w-full bg-muted rounded-full h-2">
|
||||
<div
|
||||
class="bg-primary h-2 rounded-full transition-all duration-300"
|
||||
:style="{ width: `${progressPercentage}%` }"
|
||||
></div>
|
||||
</div>
|
||||
<!-- Task Status Summary -->
|
||||
<div class="grid grid-cols-2 gap-2 text-xs">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-2 h-2 rounded-full bg-gray-400"></div>
|
||||
<span class="text-muted-foreground">Not Started: {{ taskStatusCounts.not_started }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-2 h-2 rounded-full bg-blue-500"></div>
|
||||
<span class="text-muted-foreground">In Progress: {{ taskStatusCounts.in_progress }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-2 h-2 rounded-full bg-yellow-500"></div>
|
||||
<span class="text-muted-foreground">Submitted: {{ taskStatusCounts.submitted }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-2 h-2 rounded-full bg-green-500"></div>
|
||||
<span class="text-muted-foreground">Approved: {{ taskStatusCounts.approved }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tasks List -->
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-sm font-semibold">Tasks</h3>
|
||||
<Button
|
||||
v-if="canCreateTask"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
@click="$emit('create-task')"
|
||||
>
|
||||
<Plus class="h-3 w-3 mr-1" />
|
||||
Add Task
|
||||
</Button>
|
||||
</div>
|
||||
<!-- No Tasks -->
|
||||
<div v-if="tasks.length === 0" class="text-center py-8">
|
||||
<ListTodo class="h-8 w-8 mx-auto text-muted-foreground mb-2" />
|
||||
<p class="text-sm text-muted-foreground">No tasks yet</p>
|
||||
<p class="text-xs text-muted-foreground mt-1">Create tasks to track work on this shot</p>
|
||||
</div>
|
||||
<!-- Tasks Table -->
|
||||
<div v-else class="border rounded-lg overflow-hidden">
|
||||
<div class="bg-muted/50 px-4 py-2 grid grid-cols-3 gap-4 text-xs font-medium text-muted-foreground border-b">
|
||||
<div>Task Type</div>
|
||||
<div>Assignee</div>
|
||||
<div>Status</div>
|
||||
</div>
|
||||
<div
|
||||
v-for="task in tasks"
|
||||
:key="task.id"
|
||||
class="px-4 py-3 grid grid-cols-3 gap-4 items-center hover:bg-muted/50 cursor-pointer transition-colors border-b last:border-b-0"
|
||||
@click="$emit('select-task', task)"
|
||||
>
|
||||
<div class="text-sm font-medium">{{ formatTaskType(task.task_type) }}</div>
|
||||
<div class="text-sm text-muted-foreground">
|
||||
{{ task.assigned_user_name || 'Unassigned' }}
|
||||
</div>
|
||||
<div>
|
||||
<Badge :variant="getTaskStatusVariant(task.status)" class="text-xs">
|
||||
{{ formatTaskStatus(task.status.toString()) }}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<!-- Notes Tab -->
|
||||
<TabsContent value="notes" class="flex-1 p-6 space-y-4">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-sm font-semibold">Production Notes</h3>
|
||||
<Button
|
||||
v-if="canCreateNote"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
@click="$emit('create-note')"
|
||||
>
|
||||
<Plus class="h-3 w-3 mr-1" />
|
||||
Add Note
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="text-center py-8">
|
||||
<MessageSquare class="h-8 w-8 mx-auto text-muted-foreground mb-2" />
|
||||
<p class="text-sm text-muted-foreground">No notes yet</p>
|
||||
<p class="text-xs text-muted-foreground mt-1">Add notes to track important information</p>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<!-- Assets Tab -->
|
||||
<TabsContent value="assets" class="flex-1 p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-sm font-semibold">Linked Assets</h3>
|
||||
<Button
|
||||
v-if="canLinkAssets"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
@click="$emit('link-asset')"
|
||||
>
|
||||
<Plus class="h-3 w-3 mr-1" />
|
||||
Link Asset
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="text-center py-8">
|
||||
<Package class="h-8 w-8 mx-auto text-muted-foreground mb-2" />
|
||||
<p class="text-sm text-muted-foreground">No assets linked</p>
|
||||
<p class="text-xs text-muted-foreground mt-1">Link assets that are used in this shot</p>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<!-- References Tab -->
|
||||
<TabsContent value="references" class="flex-1 p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-sm font-semibold">Reference Files</h3>
|
||||
<Button
|
||||
v-if="canUploadReferences"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
@click="$emit('upload-reference')"
|
||||
>
|
||||
<Plus class="h-3 w-3 mr-1" />
|
||||
Upload Reference
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="text-center py-8">
|
||||
<Image class="h-8 w-8 mx-auto text-muted-foreground mb-2" />
|
||||
<p class="text-sm text-muted-foreground">No reference files</p>
|
||||
<p class="text-xs text-muted-foreground mt-1">Upload images, videos, or documents for reference</p>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<!-- Design Tab -->
|
||||
<TabsContent value="design" class="flex-1 p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-sm font-semibold">Design Information</h3>
|
||||
<Button
|
||||
v-if="canEditDesign"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
@click="$emit('edit-design')"
|
||||
>
|
||||
<Edit class="h-3 w-3 mr-1" />
|
||||
Edit Design
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<Label class="text-xs text-muted-foreground">Camera Notes</Label>
|
||||
<p class="text-sm mt-1 text-muted-foreground">No camera notes</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label class="text-xs text-muted-foreground">Lighting Notes</Label>
|
||||
<p class="text-sm mt-1 text-muted-foreground">No lighting notes</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label class="text-xs text-muted-foreground">Animation Notes</Label>
|
||||
<p class="text-sm mt-1 text-muted-foreground">No animation notes</p>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import {
|
||||
AlertCircle, RefreshCw, ListTodo, Plus, MessageSquare, Package, Image, X, Edit
|
||||
} from 'lucide-vue-next'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
|
||||
import { shotService, ShotStatus, type Shot, type TaskStatusInfo, TaskStatus } from '@/services/shot'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
// Use TaskStatusInfo from shot service instead of local Task interface
|
||||
interface Task extends TaskStatusInfo {
|
||||
id: number
|
||||
name?: string
|
||||
assigned_user_name?: string
|
||||
deadline?: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
projectId: number
|
||||
shotId: number
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'edit', shot: Shot): void
|
||||
(e: 'delete', shot: Shot): void
|
||||
(e: 'create-task'): void
|
||||
(e: 'select-task', task: Task): void
|
||||
(e: 'create-note'): void
|
||||
(e: 'link-asset'): void
|
||||
(e: 'upload-reference'): void
|
||||
(e: 'edit-design'): void
|
||||
(e: 'close'): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// Reactive state
|
||||
const shot = ref<Shot | null>(null)
|
||||
const tasks = ref<Task[]>([])
|
||||
const isLoading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
// Computed properties
|
||||
const frameCount = computed(() => {
|
||||
if (!shot.value) return 0
|
||||
return shot.value.frame_end - shot.value.frame_start + 1
|
||||
})
|
||||
|
||||
const completedTasksCount = computed(() => {
|
||||
return tasks.value.filter(task => task.status.toString() === 'approved').length
|
||||
})
|
||||
|
||||
const progressPercentage = computed(() => {
|
||||
if (tasks.value.length === 0) return 0
|
||||
return Math.round((completedTasksCount.value / tasks.value.length) * 100)
|
||||
})
|
||||
|
||||
const taskStatusCounts = computed(() => {
|
||||
const counts = {
|
||||
not_started: 0,
|
||||
in_progress: 0,
|
||||
submitted: 0,
|
||||
approved: 0,
|
||||
retake: 0
|
||||
}
|
||||
|
||||
tasks.value.forEach(task => {
|
||||
const statusKey = task.status.toString() // Convert enum to string
|
||||
if (counts.hasOwnProperty(statusKey)) {
|
||||
counts[statusKey as keyof typeof counts]++
|
||||
}
|
||||
})
|
||||
|
||||
return counts
|
||||
})
|
||||
|
||||
|
||||
|
||||
const canCreateTask = computed(() => {
|
||||
return authStore.user?.role === 'coordinator' || authStore.user?.is_admin
|
||||
})
|
||||
|
||||
const canCreateNote = computed(() => {
|
||||
return authStore.user?.role === 'coordinator' || authStore.user?.is_admin
|
||||
})
|
||||
|
||||
const canLinkAssets = computed(() => {
|
||||
return authStore.user?.role === 'coordinator' || authStore.user?.is_admin
|
||||
})
|
||||
|
||||
const canUploadReferences = computed(() => {
|
||||
return true // All users can upload references
|
||||
})
|
||||
|
||||
const canEditDesign = computed(() => {
|
||||
return authStore.user?.role === 'coordinator' || authStore.user?.is_admin
|
||||
})
|
||||
|
||||
// Methods
|
||||
const loadShotDetails = async () => {
|
||||
try {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
shot.value = await shotService.getShot(props.shotId)
|
||||
loadTasks() // No longer async - uses embedded data
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : 'Failed to load shot details'
|
||||
console.error('Failed to load shot details:', err)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const loadTasks = () => {
|
||||
// Use task_details already embedded in shot data - no API call needed!
|
||||
if (shot.value?.task_details) {
|
||||
tasks.value = shot.value.task_details.map(taskInfo => ({
|
||||
id: taskInfo.task_id || 0,
|
||||
task_type: taskInfo.task_type,
|
||||
status: taskInfo.status,
|
||||
assigned_user_id: taskInfo.assigned_user_id,
|
||||
// Add placeholder values for display compatibility
|
||||
name: taskInfo.task_type, // Use task_type as name for display
|
||||
assigned_user_name: undefined // Will be resolved if needed
|
||||
}))
|
||||
} else {
|
||||
tasks.value = []
|
||||
}
|
||||
}
|
||||
|
||||
const formatStatus = (status: ShotStatus) => {
|
||||
return status.split('_').map(word =>
|
||||
word.charAt(0).toUpperCase() + word.slice(1)
|
||||
).join(' ')
|
||||
}
|
||||
|
||||
const formatTaskStatus = (status: string) => {
|
||||
return status.split('_').map(word =>
|
||||
word.charAt(0).toUpperCase() + word.slice(1)
|
||||
).join(' ')
|
||||
}
|
||||
|
||||
const formatTaskType = (taskType: string) => {
|
||||
return taskType.split('_').map(word =>
|
||||
word.charAt(0).toUpperCase() + word.slice(1)
|
||||
).join(' ')
|
||||
}
|
||||
|
||||
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 getStatusColor = (status: ShotStatus) => {
|
||||
switch (status) {
|
||||
case ShotStatus.NOT_STARTED:
|
||||
return 'bg-gray-400'
|
||||
case ShotStatus.IN_PROGRESS:
|
||||
return 'bg-blue-500'
|
||||
case ShotStatus.ON_HOLD:
|
||||
return 'bg-yellow-500'
|
||||
case ShotStatus.COMPLETED:
|
||||
return 'bg-green-500'
|
||||
case ShotStatus.APPROVED:
|
||||
return 'bg-emerald-600'
|
||||
default:
|
||||
return 'bg-gray-400'
|
||||
}
|
||||
}
|
||||
|
||||
const getTaskStatusVariant = (status: string | TaskStatus) => {
|
||||
const statusStr = status.toString()
|
||||
switch (statusStr) {
|
||||
case 'not_started':
|
||||
return 'secondary'
|
||||
case 'in_progress':
|
||||
return 'default'
|
||||
case 'submitted':
|
||||
return 'outline'
|
||||
case 'approved':
|
||||
return 'default'
|
||||
case 'retake':
|
||||
return 'destructive'
|
||||
default:
|
||||
return 'secondary'
|
||||
}
|
||||
}
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
})
|
||||
}
|
||||
|
||||
const formatDeletedDate = (deletedAt: string) => {
|
||||
const date = new Date(deletedAt)
|
||||
const now = new Date()
|
||||
const diffInHours = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60))
|
||||
|
||||
if (diffInHours < 24) {
|
||||
return `${diffInHours}h ago`
|
||||
} else {
|
||||
const diffInDays = Math.floor(diffInHours / 24)
|
||||
return `${diffInDays}d ago`
|
||||
}
|
||||
}
|
||||
|
||||
// Watchers
|
||||
watch(() => props.shotId, (newShotId) => {
|
||||
if (newShotId) {
|
||||
loadShotDetails()
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// Expose methods for parent component
|
||||
defineExpose({
|
||||
refresh: loadShotDetails
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user