LinkDesk/frontend/src/components/asset/AssetDeleteConfirmDialog.vue

297 lines
11 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 Asset: {{ assetName }}
</DialogTitle>
<DialogDescription>
This will mark the asset 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 -->
<div v-else-if="loadError" class="rounded-lg border border-destructive/20 bg-destructive/5 p-4">
<div class="flex items-center gap-2 mb-2">
<AlertCircle class="h-4 w-4 text-destructive" />
<span class="font-medium text-destructive">Failed to load deletion information</span>
</div>
<p class="text-sm text-muted-foreground">{{ loadError }}</p>
</div>
<!-- Deletion Information -->
<div v-else-if="deletionInfo" class="space-y-4">
<!-- Asset Info -->
<div class="rounded-lg border bg-muted/10 p-4">
<div class="flex items-center gap-2 mb-2">
<Package class="h-4 w-4 text-muted-foreground" />
<span class="font-medium">{{ deletionInfo.asset_name }}</span>
<Badge variant="outline" class="text-xs">{{ formatCategory(deletionInfo.asset_category) }}</Badge>
</div>
<p class="text-sm text-muted-foreground">{{ deletionInfo.project_name }}</p>
</div>
<!-- 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 -->
<div v-if="deletionInfo.affected_users.length > 0" class="rounded-lg border border-orange-200 bg-orange-50 p-4">
<div class="flex items-center gap-2 mb-3">
<Users class="h-4 w-4 text-orange-600" />
<span class="font-medium text-orange-800">
{{ deletionInfo.affected_users.length }} user{{ deletionInfo.affected_users.length === 1 ? '' : 's' }} will be affected
</span>
</div>
<p class="text-sm text-orange-700 mb-3">
The following users have work associated with this asset 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>
</div>
<!-- No affected users -->
<div v-else class="rounded-lg border border-green-200 bg-green-50 p-4">
<div class="flex items-center gap-2">
<CheckCircle class="h-4 w-4 text-green-600" />
<span class="text-sm text-green-800">No users will be affected by this deletion.</span>
</div>
</div>
<!-- Data Preservation Notice -->
<div class="rounded-lg border border-blue-200 bg-blue-50 p-4">
<div class="flex items-center gap-2 mb-2">
<Shield class="h-4 w-4 text-blue-600" />
<span class="font-medium text-blue-800">Data Preservation</span>
</div>
<p class="text-sm 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.
</p>
</div>
<!-- Confirmation input -->
<div class="space-y-2">
<Label for="confirm-input">
Type <code class="bg-muted px-1 py-0.5 rounded text-sm">{{ assetName }}</code> to confirm soft deletion:
</Label>
<Input
id="confirm-input"
v-model="confirmationText"
placeholder="Enter asset 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 Asset
<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,
Package
} from 'lucide-vue-next'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Badge } from '@/components/ui/badge'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { assetService, type AssetDeletionInfo } from '@/services/asset'
import { useToast } from '@/components/ui/toast/use-toast'
interface Props {
open: boolean
assetId: number
assetName: 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<AssetDeletionInfo | null>(null)
const isConfirmed = computed(() => {
return confirmationText.value === props.assetName
})
// Load deletion info when dialog opens
const loadDeletionInfo = async () => {
if (!props.assetId) return
isLoadingInfo.value = true
loadError.value = null
try {
deletionInfo.value = await assetService.getAssetDeletionInfo(props.assetId)
} 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'
})
}
// Format category for display
const formatCategory = (category: string): string => {
return category.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase())
}
// 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>