LinkDesk/frontend/src/components/shot/ShotDeleteConfirmDialog.vue

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
}
})
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>