280 lines
9.8 KiB
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> |