cd3628a255
Phase 2 of frontend_tasks.md - close feature gaps between domains: - Asset user-assignment popover, ported from shot's EditableTaskStatus, wired through columns.ts and AssetBrowser.vue. - Asset column-locking toggle: two-pane frozen-column layout ported into AssetsDataTable.vue (adapted for asset's page-scroll layout, which has no bounded-height container like shot's). - Task table row-actions menu (View Details + Reassign only - no Delete/Edit, since no backend support exists for either). - Real select-task/create-task behavior on asset and shot detail panels: clicking a task swaps in the actual TaskDetailPanel in place; "Add Task" opens a task-type picker that creates real tasks via the existing createAssetTask/createShotTask services. - create-note/upload-reference/publish-version implemented via a task-picker that deep-links into TaskDetailPanel's Notes/ Attachments/Submissions tabs (new initialTab prop), reusing the already-working task-level components instead of building three new bespoke forms. Also fixed shot's pre-existing dead "Add Note"/ "Upload Reference" buttons the same way. - Consolidated ProjectMembersManager.vue and ProjectMemberManagement.vue into one component, combining remove-confirmation and approved-user filtering with toast feedback and the shared Select/Dialog UI kit. - Wired ProjectDetailView's "Manage Members" to navigate to the project's Settings > Team tab. Also fixed a bug in this session's own new code: TaskBrowser.vue's row-click handler is a no-op by design, so the new row-actions menu needed to emit row-double-click (which actually opens the panel) instead. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
185 lines
5.4 KiB
Vue
185 lines
5.4 KiB
Vue
<template>
|
|
<div class="flex flex-col h-full">
|
|
|
|
<!-- Project Header -->
|
|
<div class="p-0 sm:p-0 pb-0" v-if="project">
|
|
<!-- <div class="flex flex-col sm:flex-row sm:items-start justify-between gap-4 mb-4"> -->
|
|
<!-- <div class="flex-1 min-w-0">
|
|
<h1 class="text-2xl sm:text-3xl font-bold truncate">{{ project.name }}</h1>
|
|
<p class="text-muted-foreground text-sm sm:text-base mt-1">{{ project.description }}</p>
|
|
</div> -->
|
|
<!-- <div class="flex items-center gap-2 flex-shrink-0">
|
|
<Badge :variant="getStatusVariant(project.status)">
|
|
{{ formatStatus(project.status) }}
|
|
</Badge>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="outline" size="sm">
|
|
<MoreHorizontal class="h-4 w-4" />
|
|
<span class="sr-only">Project actions</span>
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
<DropdownMenuItem @click="editProject">
|
|
<Edit class="h-4 w-4 mr-2" />
|
|
Edit Project
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem @click="manageTechnicalSpecs">
|
|
<Settings class="h-4 w-4 mr-2" />
|
|
Technical Specs
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem @click="manageMembers">
|
|
<Users class="h-4 w-4 mr-2" />
|
|
Manage Members
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div> -->
|
|
<!-- </div> -->
|
|
</div>
|
|
|
|
<!-- Project Tabs -->
|
|
<div class="px-0 sm:px-0 pb-0 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60" v-if="project">
|
|
<ProjectTabs
|
|
:project-id="project.id"
|
|
:shot-count="project.shot_count"
|
|
:asset-count="project.asset_count"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Tab Content Area -->
|
|
<div class="flex-1 overflow-auto bg-muted/30">
|
|
<router-view :key="route.fullPath" />
|
|
</div>
|
|
|
|
<!-- 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 project...</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Error State -->
|
|
<div v-else-if="error" class="text-center py-12">
|
|
<AlertCircle class="h-12 w-12 mx-auto text-destructive mb-4" />
|
|
<h3 class="text-lg font-semibold mb-2">Failed to load project</h3>
|
|
<p class="text-muted-foreground mb-4">{{ error }}</p>
|
|
<Button @click="loadProject" variant="outline">
|
|
<RefreshCw class="h-4 w-4 mr-2" />
|
|
Try Again
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed, onMounted, watch } from 'vue'
|
|
import { useRouter, useRoute } from 'vue-router'
|
|
import {
|
|
MoreHorizontal, Edit, Settings, Users,
|
|
AlertCircle, RefreshCw
|
|
} from 'lucide-vue-next'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Badge } from '@/components/ui/badge'
|
|
import {
|
|
DropdownMenu, DropdownMenuContent, DropdownMenuItem,
|
|
DropdownMenuTrigger
|
|
} from '@/components/ui/dropdown-menu'
|
|
import { useProjectsStore } from '@/stores/projects'
|
|
import ProjectTabs from '@/components/project/ProjectTabs.vue'
|
|
import type { Project } from '@/stores/projects'
|
|
|
|
const router = useRouter()
|
|
const route = useRoute()
|
|
const projectsStore = useProjectsStore()
|
|
|
|
// Reactive state
|
|
const project = ref<Project | null>(null)
|
|
const isLoading = ref(false)
|
|
const error = ref<string | null>(null)
|
|
|
|
// Computed properties
|
|
const projectId = computed(() => {
|
|
const id = route.params.projectId
|
|
return typeof id === 'string' ? parseInt(id) : null
|
|
})
|
|
|
|
|
|
|
|
// Methods
|
|
const loadProject = async () => {
|
|
if (!projectId.value) return
|
|
|
|
try {
|
|
isLoading.value = true
|
|
error.value = null
|
|
|
|
// Try to get project from store first
|
|
let projectData = projectsStore.getProjectById(projectId.value)
|
|
|
|
if (!projectData) {
|
|
// If not in store, fetch from API
|
|
projectData = await projectsStore.getProject(projectId.value, true)
|
|
}
|
|
|
|
project.value = projectData
|
|
|
|
// Set as active project in store
|
|
if (projectData) {
|
|
projectsStore.setActiveProject(projectData)
|
|
}
|
|
} catch (err) {
|
|
error.value = err instanceof Error ? err.message : 'Failed to load project'
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
const editProject = () => {
|
|
router.push('/projects')
|
|
}
|
|
|
|
const manageTechnicalSpecs = () => {
|
|
if (projectId.value) {
|
|
router.push(`/projects/${projectId.value}/technical-specs`)
|
|
}
|
|
}
|
|
|
|
const manageMembers = () => {
|
|
if (projectId.value) {
|
|
router.push(`/projects/${projectId.value}/settings`)
|
|
}
|
|
}
|
|
|
|
const getStatusVariant = (status: string) => {
|
|
switch (status) {
|
|
case 'planning': return 'secondary'
|
|
case 'in_progress': return 'default'
|
|
case 'on_hold': return 'outline'
|
|
case 'completed': return 'default'
|
|
case 'cancelled': return 'destructive'
|
|
default: return 'secondary'
|
|
}
|
|
}
|
|
|
|
const formatStatus = (status: string) => {
|
|
return status.split('_').map(word =>
|
|
word.charAt(0).toUpperCase() + word.slice(1)
|
|
).join(' ')
|
|
}
|
|
|
|
// Watchers
|
|
watch(projectId, (newId) => {
|
|
if (newId) {
|
|
loadProject()
|
|
}
|
|
}, { immediate: true })
|
|
|
|
// Lifecycle
|
|
onMounted(() => {
|
|
if (projectId.value) {
|
|
loadProject()
|
|
}
|
|
})
|
|
</script> |