281 lines
7.7 KiB
Vue
281 lines
7.7 KiB
Vue
<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> |