Files
LinkDesk/frontend/src/components/shot/ShotDeleteConfirmDialog.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

280 lines
9.8 KiB
Vue

<template>
<Dialog :open="open" @update:open="$emit('update:open', $event)">
<DialogContent class="max-w-3xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle class="flex items-center gap-2">
<AlertTriangle class="h-5 w-5 text-destructive" />
Soft Delete Shot: {{ shotName }}
</DialogTitle>
<DialogDescription>
This will mark the shot and all related data as deleted while preserving it for potential recovery.
The data will be hidden from normal operations but can be restored by administrators.
</DialogDescription>
</DialogHeader>
<!-- Loading State -->
<div v-if="isLoadingInfo" class="flex items-center justify-center py-8">
<div class="flex items-center gap-2">
<Loader2 class="h-4 w-4 animate-spin" />
<span class="text-sm text-muted-foreground">Loading deletion information...</span>
</div>
</div>
<!-- Error State -->
<Alert v-else-if="loadError" variant="destructive">
<AlertCircle class="h-4 w-4" />
<AlertTitle>Failed to load deletion information</AlertTitle>
<AlertDescription>{{ loadError }}</AlertDescription>
</Alert>
<!-- Deletion Information -->
<div v-else-if="deletionInfo" class="space-y-4">
<!-- Impact Summary -->
<div class="rounded-lg border bg-muted/20 p-4">
<h3 class="font-medium mb-3">Deletion Impact Summary</h3>
<div class="grid grid-cols-2 md:grid-cols-3 gap-4 text-sm">
<div class="flex items-center gap-2">
<ListTodo class="h-4 w-4 text-muted-foreground" />
<span>{{ deletionInfo.task_count }} task{{ deletionInfo.task_count === 1 ? '' : 's' }}</span>
</div>
<div class="flex items-center gap-2">
<Upload class="h-4 w-4 text-muted-foreground" />
<span>{{ deletionInfo.submission_count }} submission{{ deletionInfo.submission_count === 1 ? '' : 's' }}</span>
</div>
<div class="flex items-center gap-2">
<Paperclip class="h-4 w-4 text-muted-foreground" />
<span>{{ deletionInfo.attachment_count }} attachment{{ deletionInfo.attachment_count === 1 ? '' : 's' }}</span>
</div>
<div class="flex items-center gap-2">
<MessageSquare class="h-4 w-4 text-muted-foreground" />
<span>{{ deletionInfo.note_count }} note{{ deletionInfo.note_count === 1 ? '' : 's' }}</span>
</div>
<div class="flex items-center gap-2">
<CheckCircle class="h-4 w-4 text-muted-foreground" />
<span>{{ deletionInfo.review_count }} review{{ deletionInfo.review_count === 1 ? '' : 's' }}</span>
</div>
<div class="flex items-center gap-2">
<HardDrive class="h-4 w-4 text-muted-foreground" />
<span>{{ formatFileSize(deletionInfo.total_file_size) }} files</span>
</div>
</div>
</div>
<!-- Affected Users -->
<Alert v-if="deletionInfo.affected_users.length > 0" variant="default" class="border-orange-200 bg-orange-50">
<Users class="h-4 w-4 text-orange-600" />
<AlertTitle class="text-orange-800">
{{ deletionInfo.affected_users.length }} user{{ deletionInfo.affected_users.length === 1 ? '' : 's' }} will be affected
</AlertTitle>
<AlertDescription class="text-orange-700">
<p class="mb-3">
The following users have work associated with this shot that will be marked as deleted:
</p>
<div class="max-h-40 overflow-y-auto space-y-2">
<div
v-for="user in deletionInfo.affected_users"
:key="user.id"
class="flex items-center justify-between p-2 bg-white rounded border"
>
<div class="flex-1">
<div class="font-medium text-sm">{{ user.name }}</div>
<div class="text-xs text-muted-foreground">{{ user.email }} {{ user.role }}</div>
</div>
<div class="text-xs text-muted-foreground text-right">
<div v-if="user.task_count > 0">{{ user.task_count }} task{{ user.task_count === 1 ? '' : 's' }}</div>
<div v-if="user.submission_count > 0">{{ user.submission_count }} submission{{ user.submission_count === 1 ? '' : 's' }}</div>
<div v-if="user.note_count > 0">{{ user.note_count }} note{{ user.note_count === 1 ? '' : 's' }}</div>
<div v-if="user.last_activity_date" class="mt-1">
Last active: {{ formatDate(user.last_activity_date) }}
</div>
</div>
</div>
</div>
</AlertDescription>
</Alert>
<!-- No affected users -->
<Alert v-else variant="default" class="border-green-200 bg-green-50">
<CheckCircle class="h-4 w-4 text-green-600" />
<AlertDescription class="text-green-800">
No users will be affected by this deletion.
</AlertDescription>
</Alert>
<!-- Data Preservation Notice -->
<Alert variant="default" class="border-blue-200 bg-blue-50">
<Shield class="h-4 w-4 text-blue-600" />
<AlertTitle class="text-blue-800">Data Preservation</AlertTitle>
<AlertDescription class="text-blue-700">
All data will be preserved in the database and can be recovered by administrators.
Files will remain on the server unchanged. This is a soft deletion, not permanent removal.
</AlertDescription>
</Alert>
<!-- Confirmation input -->
<div class="space-y-2">
<Label for="confirm-input">
Type <code class="bg-muted px-1 py-0.5 rounded text-sm">{{ shotName }}</code> to confirm soft deletion:
</Label>
<Input
id="confirm-input"
v-model="confirmationText"
placeholder="Enter shot name to confirm"
class="font-mono"
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" @click="$emit('update:open', false)">
Cancel
</Button>
<Button
variant="destructive"
:disabled="!isConfirmed || isDeleting || isLoadingInfo || !!loadError"
@click="handleDelete"
>
<Loader2 v-if="isDeleting" class="mr-2 h-4 w-4 animate-spin" />
Soft Delete Shot
<span v-if="deletionInfo && deletionInfo.task_count > 0">
and {{ deletionInfo.task_count }} Task{{ deletionInfo.task_count === 1 ? '' : 's' }}
</span>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import {
AlertTriangle,
AlertCircle,
CheckCircle,
Loader2,
ListTodo,
Upload,
Paperclip,
MessageSquare,
HardDrive,
Users,
Shield
} from 'lucide-vue-next'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
Alert,
AlertDescription,
AlertTitle,
} from '@/components/ui/alert'
import { shotService, type ShotDeletionInfo } from '@/services/shot'
import { useToast } from '@/components/ui/toast/use-toast'
interface Props {
open: boolean
shotId: number
shotName: string
}
const props = defineProps<Props>()
const emit = defineEmits<{
'update:open': [value: boolean]
'confirm-delete': []
}>()
const { toast } = useToast()
const confirmationText = ref('')
const isDeleting = ref(false)
const isLoadingInfo = ref(false)
const loadError = ref<string | null>(null)
const deletionInfo = ref<ShotDeletionInfo | null>(null)
const isConfirmed = computed(() => {
return confirmationText.value === props.shotName
})
// Load deletion info when dialog opens
const loadDeletionInfo = async () => {
if (!props.shotId) return
isLoadingInfo.value = true
loadError.value = null
try {
deletionInfo.value = await shotService.getShotDeletionInfo(props.shotId)
} catch (error) {
console.error('Failed to load deletion info:', error)
loadError.value = error instanceof Error ? error.message : 'Failed to load deletion information'
} finally {
isLoadingInfo.value = false
}
}
// Format file size for display
const formatFileSize = (bytes: number): string => {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
}
// Format date for display
const formatDate = (dateString: string): string => {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})
}
// Reset state when dialog opens/closes
watch(() => props.open, (newOpen) => {
if (newOpen) {
confirmationText.value = ''
isDeleting.value = false
deletionInfo.value = null
loadError.value = null
loadDeletionInfo()
} else {
confirmationText.value = ''
isDeleting.value = false
deletionInfo.value = null
loadError.value = null
}
}, { immediate: true })
const handleDelete = async () => {
if (!isConfirmed.value) return
isDeleting.value = true
try {
emit('confirm-delete')
} catch (error) {
console.error('Delete operation failed:', error)
toast({
title: 'Deletion failed',
description: error instanceof Error ? error.message : 'An unexpected error occurred',
variant: 'destructive'
})
} finally {
isDeleting.value = false
}
}
</script>