Init Repo
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user