Files
LinkDesk/frontend/src/views/project/ProjectOverviewView.vue
T
indigo cd2efe3587 Truth-in-UI cleanup: wire real dashboard data, fix delete/dialog gaps
Phase 1 of frontend_tasks.md - stop showing fabricated/broken UI:

- Dashboard now fetches real stats (projects, tasks, users, pending
  approvals, API keys, developer stats, pending reviews, admin
  activity) instead of hardcoded numbers. Added services/developer.ts
  and services/review.ts wrappers for previously-unused backend
  endpoints.
- Wired ActivityFeed into every project's Overview page in place of
  the "coming soon" placeholder.
- Registered the missing /projects/:id/technical-specs route (view
  and service already existed, just unreachable).
- Fixed AssetDeleteConfirmDialog's raw styled divs to use the shared
  Alert component and wired it into AssetBrowser, matching
  ShotBrowser's impact-summary + type-to-confirm safety pattern
  (asset deletion was previously less safe than shot deletion).
- Fixed a shared bug in both delete dialogs where the impact-summary
  section never rendered (watch on the open prop needed
  { immediate: true }).
- Replaced native confirm()/alert() with styled AlertDialog/Dialog in
  NoteItem, TaskAttachments, and UserMenu's keyboard-shortcuts item.
- Removed dead-end UI: Google OAuth stub buttons, UserMenu items
  pointing at non-existent routes, the /developer/docs dead link, and
  the no-op action button on the API Keys placeholder page.
- Removed leftover debug console logging across 8 files.

Added frontend_report.md (full audit) and frontend_tasks.md (phased
checklist) as the reference for this and future phases.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-17 04:47:26 +08:00

194 lines
6.6 KiB
Vue

<template>
<div class="p-4 sm:p-6">
<div class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
<!-- Project Stats -->
<Card>
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle class="text-sm font-medium">Total Shots</CardTitle>
<Camera class="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div class="text-2xl font-bold">{{ project?.shot_count || 0 }}</div>
<p class="text-xs text-muted-foreground">
Across all episodes
</p>
</CardContent>
</Card>
<Card>
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle class="text-sm font-medium">Assets</CardTitle>
<Package class="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div class="text-2xl font-bold">{{ project?.asset_count || 0 }}</div>
<p class="text-xs text-muted-foreground">
Characters, props, sets
</p>
</CardContent>
</Card>
<Card>
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle class="text-sm font-medium">Team Members</CardTitle>
<Users class="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div class="text-2xl font-bold">{{ project?.member_count || 0 }}</div>
<p class="text-xs text-muted-foreground">
Active contributors
</p>
</CardContent>
</Card>
</div>
<!-- Project Information -->
<div class="mt-6 grid gap-6 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>Project Details</CardTitle>
</CardHeader>
<CardContent class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div>
<Label class="text-sm font-medium text-muted-foreground">Code Name</Label>
<p class="font-mono">{{ project?.code_name }}</p>
</div>
<div>
<Label class="text-sm font-medium text-muted-foreground">Client</Label>
<p>{{ project?.client_name }}</p>
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<Label class="text-sm font-medium text-muted-foreground">Type</Label>
<p>{{ formatProjectType(project?.project_type) }}</p>
</div>
<div>
<Label class="text-sm font-medium text-muted-foreground">Status</Label>
<Badge :variant="getStatusVariant(project?.status)">
{{ formatStatus(project?.status) }}
</Badge>
</div>
</div>
<div v-if="project?.description">
<Label class="text-sm font-medium text-muted-foreground">Description</Label>
<p class="text-sm">{{ project.description }}</p>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Timeline</CardTitle>
</CardHeader>
<CardContent class="space-y-4">
<div v-if="project?.start_date">
<Label class="text-sm font-medium text-muted-foreground">Start Date</Label>
<p>{{ formatDate(project.start_date) }}</p>
</div>
<div v-if="project?.end_date">
<Label class="text-sm font-medium text-muted-foreground">End Date</Label>
<p>{{ formatDate(project.end_date) }}</p>
</div>
<div v-if="project?.start_date && project?.end_date">
<Label class="text-sm font-medium text-muted-foreground">Duration</Label>
<p>{{ calculateDuration(project.start_date, project.end_date) }}</p>
</div>
</CardContent>
</Card>
</div>
<!-- Technical Specifications Summary -->
<div class="mt-6" v-if="project">
<TechnicalSpecsSummary :project-id="project.id" />
</div>
<!-- Recent Activity -->
<div class="mt-6" v-if="project">
<ActivityFeed :project-id="project.id" title="Recent Activity" />
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import { Camera, Package, Users } from 'lucide-vue-next'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Label } from '@/components/ui/label'
import { useProjectsStore } from '@/stores/projects'
import TechnicalSpecsSummary from '@/components/project/TechnicalSpecsSummary.vue'
import ActivityFeed from '@/components/activity/ActivityFeed.vue'
const route = useRoute()
const projectsStore = useProjectsStore()
// Computed properties
const projectId = computed(() => {
const id = route.params.projectId
return typeof id === 'string' ? parseInt(id) : null
})
const project = computed(() => {
if (!projectId.value) return null
return projectsStore.getProjectById(projectId.value)
})
// Methods
const getStatusVariant = (status?: string) => {
if (!status) return 'secondary'
switch (status) {
case 'planning': return 'secondary'
case 'in_progress': return 'default'
case 'on_hold': return 'outline'
case 'completed': return 'success'
case 'cancelled': return 'destructive'
default: return 'secondary'
}
}
const formatStatus = (status?: string) => {
if (!status) return ''
return status.split('_').map(word =>
word.charAt(0).toUpperCase() + word.slice(1)
).join(' ')
}
const formatProjectType = (type?: string) => {
if (!type) return ''
switch (type) {
case 'tv': return 'TV Series'
case 'cinema': return 'Cinema/Film'
case 'game': return 'Game'
default: return type
}
}
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
})
}
const calculateDuration = (startDate: string, endDate: string) => {
const start = new Date(startDate)
const end = new Date(endDate)
const diffTime = Math.abs(end.getTime() - start.getTime())
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
if (diffDays < 30) {
return `${diffDays} days`
} else if (diffDays < 365) {
const months = Math.floor(diffDays / 30)
return `${months} month${months > 1 ? 's' : ''}`
} else {
const years = Math.floor(diffDays / 365)
const remainingMonths = Math.floor((diffDays % 365) / 30)
return `${years} year${years > 1 ? 's' : ''}${remainingMonths > 0 ? ` ${remainingMonths} month${remainingMonths > 1 ? 's' : ''}` : ''}`
}
}
</script>