Init Repo

This commit is contained in:
2026-02-28 03:22:04 +08:00
commit de59b57ee7
883 changed files with 156857 additions and 0 deletions
+324
View File
@@ -0,0 +1,324 @@
<template>
<div class="p-6">
<!-- Breadcrumb Navigation -->
<nav class="flex items-center space-x-2 text-sm text-muted-foreground mb-6">
<Button variant="ghost" size="sm" @click="router.push('/projects')" class="p-0 h-auto">
Projects
</Button>
<ChevronRight class="h-4 w-4" />
<Button variant="ghost" size="sm" @click="goToProject" class="p-0 h-auto" v-if="currentProject">
{{ currentProject.name }}
</Button>
<ChevronRight class="h-4 w-4" v-if="currentProject" />
<span class="font-medium text-foreground">Episodes</span>
</nav>
<!-- Header -->
<div class="flex items-center justify-between mb-6">
<div>
<h1 class="text-3xl font-bold">Episodes</h1>
<p class="text-muted-foreground" v-if="currentProject">
Manage episodes for {{ currentProject.name }}
</p>
</div>
<!-- Project Switcher -->
<div class="flex items-center gap-3">
<Select
:model-value="selectedProjectId?.toString()"
@update:model-value="handleProjectChange"
>
<SelectTrigger class="w-64">
<SelectValue placeholder="Select project..." />
</SelectTrigger>
<SelectContent>
<SelectItem
v-for="project in availableProjects"
:key="project.id"
:value="project.id.toString()"
>
{{ project.name }} ({{ project.code_name }})
</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<!-- Technical Specifications Panel -->
<div class="mb-6" v-if="selectedProjectId">
<TechnicalSpecsPanel
:project-id="selectedProjectId"
:user-department="userDepartment"
@edit="goToTechnicalSpecs"
/>
</div>
<!-- Episodes List -->
<EpisodeList
:episodes="episodes"
:is-loading="isLoading"
:error="error"
:can-create="canCreateEpisodes"
:can-delete="canDeleteEpisodes"
@create="showCreateDialog = true"
@select="selectEpisode"
@edit="editEpisode"
@view-shots="viewShots"
@delete="deleteEpisode"
@retry="loadEpisodes"
/>
<!-- Create/Edit Episode Dialog -->
<Dialog :open="showCreateDialog" @update:open="showCreateDialog = $event">
<DialogContent class="sm:max-w-md">
<DialogHeader>
<DialogTitle>{{ editingEpisode ? 'Edit Episode' : 'Create New Episode' }}</DialogTitle>
<DialogDescription>
{{ editingEpisode ? 'Update episode information' : 'Create a new episode for this project' }}
</DialogDescription>
</DialogHeader>
<EpisodeForm
:episode="editingEpisode"
:is-submitting="isSubmitting"
@submit="submitEpisode"
@cancel="cancelEpisodeForm"
/>
</DialogContent>
</Dialog>
<!-- Delete Confirmation Dialog -->
<AlertDialog :open="showDeleteDialog" @update:open="showDeleteDialog = $event">
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Episode</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete "{{ episodeToDelete?.name }}"? This action cannot be undone and will remove all associated shots and tasks.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction @click="confirmDelete" class="bg-destructive text-destructive-foreground hover:bg-destructive/90">
Delete Episode
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { ChevronRight } from 'lucide-vue-next'
import { Button } from '@/components/ui/button'
import {
Select, SelectContent, SelectItem, SelectTrigger, SelectValue
} from '@/components/ui/select'
import {
Dialog, DialogContent, DialogDescription,
DialogHeader, DialogTitle
} from '@/components/ui/dialog'
import {
AlertDialog, AlertDialogAction, AlertDialogCancel,
AlertDialogContent, AlertDialogDescription, AlertDialogFooter,
AlertDialogHeader, AlertDialogTitle
} from '@/components/ui/alert-dialog'
import { useToast } from '@/components/ui/toast/use-toast'
import { useAuthStore } from '@/stores/auth'
import { useProjectsStore } from '@/stores/projects'
import { episodeService, type Episode, type EpisodeCreate, type EpisodeUpdate } from '@/services/episode'
import EpisodeList from '@/components/episode/EpisodeList.vue'
import EpisodeForm from '@/components/episode/EpisodeForm.vue'
import TechnicalSpecsPanel from '@/components/project/TechnicalSpecsPanel.vue'
const router = useRouter()
const route = useRoute()
const { toast } = useToast()
const authStore = useAuthStore()
const projectsStore = useProjectsStore()
// Reactive state
const episodes = ref<Episode[]>([])
const isLoading = ref(false)
const error = ref<string | null>(null)
const showCreateDialog = ref(false)
const showDeleteDialog = ref(false)
const editingEpisode = ref<Episode | null>(null)
const episodeToDelete = ref<Episode | null>(null)
const isSubmitting = ref(false)
const selectedProjectId = ref<number | null>(null)
// Computed properties
const canCreateEpisodes = computed(() => {
const userRole = authStore.userRole
const isAdmin = authStore.user?.is_admin
return userRole === 'coordinator' || isAdmin
})
const canDeleteEpisodes = computed(() => {
const userRole = authStore.userRole
const isAdmin = authStore.user?.is_admin
return userRole === 'coordinator' || isAdmin
})
const availableProjects = computed(() => {
return projectsStore.projects
})
const currentProject = computed(() => {
if (!selectedProjectId.value) return null
return projectsStore.projects.find(p => p.id === selectedProjectId.value)
})
const userDepartment = computed(() => {
if (!currentProject.value?.project_members || !authStore.user) return undefined
const member = currentProject.value.project_members.find(m => m.user_id === authStore.user?.id)
return member?.department_role
})
// Methods
const loadProjects = async () => {
try {
await projectsStore.fetchProjects()
// Set initial project from route params or first available project
const projectIdFromRoute = route.params.projectId ? parseInt(route.params.projectId as string) : null
if (projectIdFromRoute && projectsStore.projects.some(p => p.id === projectIdFromRoute)) {
selectedProjectId.value = projectIdFromRoute
} else if (projectsStore.projects.length > 0) {
selectedProjectId.value = projectsStore.projects[0].id
// Redirect to the correct URL with the selected project
router.replace(`/projects/${projectsStore.projects[0].id}/episodes`)
}
} catch (err) {
error.value = err instanceof Error ? err.message : 'Failed to load projects'
}
}
const loadEpisodes = async () => {
if (!selectedProjectId.value) return
try {
isLoading.value = true
error.value = null
episodes.value = await episodeService.getProjectEpisodes(selectedProjectId.value)
} catch (err) {
error.value = err instanceof Error ? err.message : 'Failed to load episodes'
} finally {
isLoading.value = false
}
}
const handleProjectChange = (projectId: string) => {
const id = parseInt(projectId)
selectedProjectId.value = id
// Update URL to reflect selected project
router.replace(`/projects/${id}/episodes`)
}
const goToProject = () => {
if (currentProject.value) {
router.push('/projects')
}
}
const selectEpisode = (episode: Episode) => {
// Navigate to episode detail or shots view
router.push(`/projects/${selectedProjectId.value}/episodes/${episode.id}/shots`)
}
const editEpisode = (episode: Episode) => {
editingEpisode.value = episode
showCreateDialog.value = true
}
const viewShots = (episode: Episode) => {
router.push(`/projects/${selectedProjectId.value}/episodes/${episode.id}/shots`)
}
const deleteEpisode = (episode: Episode) => {
episodeToDelete.value = episode
showDeleteDialog.value = true
}
const submitEpisode = async (data: EpisodeCreate | EpisodeUpdate) => {
if (!selectedProjectId.value) return
try {
isSubmitting.value = true
if (editingEpisode.value) {
await episodeService.updateEpisode(editingEpisode.value.id, data as EpisodeUpdate)
toast({
title: 'Episode updated',
description: 'Episode has been updated successfully.'
})
} else {
await episodeService.createEpisode(selectedProjectId.value, data as EpisodeCreate)
toast({
title: 'Episode created',
description: 'New episode has been created successfully.'
})
}
cancelEpisodeForm()
await loadEpisodes()
} catch (err) {
toast({
title: 'Error',
description: err instanceof Error ? err.message : 'Failed to save episode',
variant: 'destructive'
})
} finally {
isSubmitting.value = false
}
}
const cancelEpisodeForm = () => {
showCreateDialog.value = false
editingEpisode.value = null
}
const confirmDelete = async () => {
if (!episodeToDelete.value) return
try {
await episodeService.deleteEpisode(episodeToDelete.value.id)
toast({
title: 'Episode deleted',
description: 'Episode has been deleted successfully.'
})
await loadEpisodes()
} catch (err) {
toast({
title: 'Error',
description: err instanceof Error ? err.message : 'Failed to delete episode',
variant: 'destructive'
})
} finally {
showDeleteDialog.value = false
episodeToDelete.value = null
}
}
const goToTechnicalSpecs = () => {
if (selectedProjectId.value) {
router.push(`/projects/${selectedProjectId.value}/technical-specs`)
}
}
// Watchers
watch(selectedProjectId, (newProjectId) => {
if (newProjectId) {
loadEpisodes()
}
})
// Lifecycle
onMounted(async () => {
await loadProjects()
})
</script>