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>
This commit is contained in:
2026-07-17 04:47:26 +08:00
parent c63a71883b
commit cd2efe3587
25 changed files with 639 additions and 331 deletions
+38 -37
View File
@@ -161,26 +161,14 @@
</Dialog>
<!-- Delete Confirmation Dialog -->
<AlertDialog v-model:open="showDeleteDialog">
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Asset</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete "{{ selectedAsset?.name }}"? This
action cannot be undone and will remove all associated tasks.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
@click="handleDeleteAsset"
class="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
Delete Asset
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<AssetDeleteConfirmDialog
v-if="deletionInfo && assetToDelete"
:open="showDeleteDialog"
:asset-id="assetToDelete.id"
:asset-name="deletionInfo.asset_name"
@update:open="showDeleteDialog = $event"
@confirm-delete="handleDeleteAsset"
/>
</div>
<!-- Asset Detail Panel (Desktop) with slide animation -->
<Transition
@@ -251,20 +239,11 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { Sheet, SheetContent } from "@/components/ui/sheet";
import AssetCard from "./AssetCard.vue";
import AssetForm from "./AssetForm.vue";
import AssetDetailPanel from "./AssetDetailPanel.vue";
import AssetDeleteConfirmDialog from "./AssetDeleteConfirmDialog.vue";
import AssetsDataTable from "./AssetsDataTable.vue";
import AssetTableToolbar from "./AssetTableToolbar.vue";
import { createAssetColumns, type AssetColumnMeta } from "./columns";
@@ -273,11 +252,13 @@ import { useAuthStore } from "@/stores/auth";
import { useTaskStatusesStore } from "@/stores/taskStatuses";
import { useDetailPanel } from "@/composables/useDetailPanel";
import {
assetService,
AssetCategory,
TaskStatus,
type Asset,
type AssetCreate,
type AssetUpdate,
type AssetDeletionInfo,
} from "@/services/asset";
import { useToast } from "@/components/ui/toast/use-toast";
import type { SortingState, VisibilityState } from '@tanstack/vue-table';
@@ -385,6 +366,9 @@ const showDeleteDialog = ref(false);
const isCreating = ref(false);
const isUpdating = ref(false);
const assetToDelete = ref<Asset | null>(null);
const deletionInfo = ref<AssetDeletionInfo | null>(null);
const taskStatusFilter = ref('')
// Thumbnail display state - with session storage
@@ -563,9 +547,20 @@ const editAsset = (asset: Asset) => {
showEditDialog.value = true;
};
const deleteAsset = (asset: Asset) => {
selectedAsset.value = asset;
showDeleteDialog.value = true;
const deleteAsset = async (asset: Asset) => {
// Don't set selectedAsset here as it opens the detail panel
assetToDelete.value = asset;
try {
deletionInfo.value = await assetService.getAssetDeletionInfo(asset.id);
showDeleteDialog.value = true;
} catch (err) {
toast({
title: "Failed to get asset information",
description: err instanceof Error ? err.message : "An error occurred",
variant: "destructive",
});
}
};
const viewAssetTasks = (asset: Asset) => {
@@ -619,15 +614,21 @@ const handleUpdateAsset = async (assetData: AssetCreate | AssetUpdate) => {
};
const handleDeleteAsset = async () => {
if (!selectedAsset.value) return;
if (!assetToDelete.value) return;
try {
await assetsStore.deleteAsset(selectedAsset.value.id);
await assetsStore.deleteAsset(assetToDelete.value.id);
showDeleteDialog.value = false;
selectedAsset.value = null;
assetToDelete.value = null;
const taskCount = deletionInfo.value?.task_count || 0;
deletionInfo.value = null;
toast({
title: "Asset deleted",
description: "Asset has been deleted successfully.",
description: taskCount > 0
? `Asset and ${taskCount} associated task${taskCount === 1 ? '' : 's'} deleted successfully.`
: "Asset has been deleted successfully.",
});
} catch (err) {
toast({
@@ -21,13 +21,11 @@
</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>
<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">
@@ -73,59 +71,56 @@
</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>
<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 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 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>
</div>
</AlertDescription>
</Alert>
<!-- 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>
<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 -->
<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.
<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.
</p>
</div>
</AlertDescription>
</Alert>
<!-- Confirmation input -->
<div class="space-y-2">
@@ -189,6 +184,11 @@ import {
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
Alert,
AlertDescription,
AlertTitle,
} from '@/components/ui/alert'
import { assetService, type AssetDeletionInfo } from '@/services/asset'
import { useToast } from '@/components/ui/toast/use-toast'
@@ -275,7 +275,7 @@ watch(() => props.open, (newOpen) => {
deletionInfo.value = null
loadError.value = null
}
})
}, { immediate: true })
const handleDelete = async () => {
if (!isConfirmed.value) return
@@ -10,14 +10,9 @@
<SelectTrigger class="h-6 w-[130px] font-semibold text-xs"
:style="{ backgroundColor: currentStatusObject.color }"
>
<SelectValue
<SelectValue
:model-value="currentStatusId"
>
<!-- <TaskStatusBadge :status="currentStatusObject"/> -->
<!-- console.log(currentStatusObject) -->
<!-- currentStatusObject -->
</SelectValue>
/>
</SelectTrigger>
<SelectContent>
<SelectItem
@@ -119,7 +119,6 @@ import {
Key,
Database,
BarChart3,
FileText,
RotateCcw,
} from 'lucide-vue-next'
@@ -183,8 +182,7 @@ const developerItems = computed(() => [
{ title: 'API Keys', url: '/developer/api-keys', icon: Key },
{ title: 'All Projects', url: '/developer/projects', icon: Database },
{ title: 'All Tasks', url: '/developer/tasks', icon: CheckSquare },
{ title: 'Usage Analytics', url: '/developer/analytics', icon: BarChart3 },
{ title: 'Documentation', url: '/developer/docs', icon: FileText }
{ title: 'Usage Analytics', url: '/developer/analytics', icon: BarChart3 }
])
// Mock recent projects - this would come from a store in real implementation
+27 -38
View File
@@ -65,26 +65,11 @@
<User class="size-4" />
Profile
</DropdownMenuItem>
<DropdownMenuItem @click="navigateTo('/settings/preferences')">
<Palette class="size-4" />
Preferences
</DropdownMenuItem>
<DropdownMenuItem @click="toggleNotifications">
<Bell class="size-4" />
Notifications
<span class="ml-auto text-xs text-muted-foreground">
{{ notificationsEnabled ? 'On' : 'Off' }}
</span>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<!-- Help and support -->
<DropdownMenuGroup>
<DropdownMenuItem @click="navigateTo('/help')">
<HelpCircle class="size-4" />
Help & Support
</DropdownMenuItem>
<DropdownMenuItem @click="showKeyboardShortcuts">
<Keyboard class="size-4" />
Keyboard Shortcuts
@@ -99,6 +84,24 @@
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Dialog v-model:open="showShortcutsDialog">
<DialogContent class="sm:max-w-md">
<DialogHeader>
<DialogTitle>Keyboard Shortcuts</DialogTitle>
</DialogHeader>
<div class="grid gap-2 text-sm">
<div class="flex justify-between">
<span class="text-muted-foreground">Toggle sidebar</span>
<span class="font-mono">/Ctrl + B</span>
</div>
<div class="flex justify-between">
<span class="text-muted-foreground">Quick search</span>
<span class="font-mono">/Ctrl + K</span>
</div>
</div>
</DialogContent>
</Dialog>
</SidebarMenuItem>
</SidebarMenu>
</template>
@@ -111,11 +114,8 @@ import {
LogOut,
User,
Settings,
Bell,
Key,
Users,
Palette,
HelpCircle,
Keyboard,
} from 'lucide-vue-next'
import {
@@ -132,6 +132,12 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
SidebarMenu,
SidebarMenuButton,
@@ -181,8 +187,7 @@ const showRoleFeatures = computed(() => {
return user.value?.role === 'developer' || isAdminOrCoordinator.value || user.value?.is_admin
})
// Notifications state (this would typically come from a notifications store)
const notificationsEnabled = ref(true)
const showShortcutsDialog = ref(false)
// Actions
const navigateTo = (path: string) => {
@@ -198,24 +203,8 @@ const handleLogout = async () => {
}
}
const toggleNotifications = () => {
notificationsEnabled.value = !notificationsEnabled.value
// In a real app, this would update user preferences
console.log('Notifications toggled:', notificationsEnabled.value)
}
const showKeyboardShortcuts = () => {
// In a real app, this would open a modal with keyboard shortcuts
console.log('Keyboard shortcuts modal would open here')
// For now, just show an alert with some common shortcuts
alert(`Keyboard Shortcuts:
⌘/Ctrl + B - Toggle sidebar
⌘/Ctrl + K - Quick search
⌘/Ctrl + , - Open preferences
⌘/Ctrl + / - Show help
More shortcuts available in the help documentation.`)
showShortcutsDialog.value = true
}
@@ -102,49 +102,31 @@ const tabs = computed<Tab[]>(() => [
// Determine active tab based on current route
const activeTab = computed(() => {
const currentPath = route.path;
// Debug logging
console.log('ProjectTabs - Current path:', currentPath);
console.log('ProjectTabs - Project ID:', props.projectId);
if (currentPath === `/projects/${props.projectId}`) {
console.log('ProjectTabs - Active tab: overview');
return "overview";
} else if (currentPath.startsWith(`/projects/${props.projectId}/shots`)) {
console.log('ProjectTabs - Active tab: shots');
return "shots";
} else if (currentPath.startsWith(`/projects/${props.projectId}/assets`)) {
console.log('ProjectTabs - Active tab: assets');
return "assets";
} else if (currentPath.startsWith(`/projects/${props.projectId}/tasks`)) {
console.log('ProjectTabs - Active tab: tasks');
return "tasks";
} else if (
currentPath.startsWith(`/projects/${props.projectId}/settings`)
) {
console.log('ProjectTabs - Active tab: settings');
return "settings";
}
console.log('ProjectTabs - Active tab: overview (default)');
return "overview";
});
// Set active tab and navigate
const setActiveTab = (tabId: string) => {
const tab = tabs.value.find((t) => t.id === tabId);
console.log('ProjectTabs - Setting active tab:', tabId);
console.log('ProjectTabs - Tab found:', tab);
console.log('ProjectTabs - Current route path:', route.path);
console.log('ProjectTabs - Target route:', tab?.route);
if (tab) {
console.log('ProjectTabs - Navigating to:', tab.route);
router.push(tab.route).catch(err => {
console.error('ProjectTabs - Navigation error:', err);
router.push(tab.route).catch(() => {
// Navigation aborted (e.g. duplicate route) — safe to ignore
});
} else {
console.log('ProjectTabs - Tab not found');
}
};
@@ -428,21 +428,13 @@ const handleDialogSave = async () => {
}
const handleDelete = (category: 'asset' | 'shot', taskType: string) => {
console.log('=== HANDLE DELETE ===')
console.log('Received taskType:', taskType)
console.log('Received category:', category)
taskTypeToDelete.value = taskType
categoryToDelete.value = category
deleteError.value = ''
isDeleteDialogOpen.value = true
console.log('Set taskTypeToDelete.value to:', taskTypeToDelete.value)
console.log('Set categoryToDelete.value to:', categoryToDelete.value)
}
const closeDeleteDialog = () => {
console.log('=== CLOSE DELETE DIALOG ===')
isDeleteDialogOpen.value = false
// Values will be cleared by @update:open handler
}
@@ -451,35 +443,22 @@ const confirmDelete = async () => {
// Capture values immediately before any async operations
const taskTypeToDeleteLocal = taskTypeToDelete.value
const categoryToDeleteLocal = categoryToDelete.value
try {
isDeleting.value = true
deleteError.value = ''
console.log('=== DELETE DEBUG ===')
console.log('taskTypeToDelete.value:', taskTypeToDeleteLocal)
console.log('categoryToDelete.value:', categoryToDeleteLocal)
console.log('projectId:', props.projectId)
if (!taskTypeToDeleteLocal) {
console.error('ERROR: taskTypeToDelete is empty!')
deleteError.value = 'Task type name is missing. Please try again.'
isDeleting.value = false
return
}
console.log('Deleting task type:', {
projectId: props.projectId,
taskType: taskTypeToDeleteLocal,
category: categoryToDeleteLocal
})
const response = await customTaskTypeService.deleteCustomTaskType(
props.projectId,
taskTypeToDeleteLocal,
categoryToDeleteLocal
)
console.log('Delete task type response:', response)
taskTypes.value = response
toast({
@@ -258,7 +258,7 @@ watch(() => props.open, (newOpen) => {
deletionInfo.value = null
loadError.value = null
}
})
}, { immediate: true })
const handleDelete = async () => {
if (!isConfirmed.value) return
+34 -3
View File
@@ -93,6 +93,23 @@
</div>
</div>
</div>
<AlertDialog :open="showDeleteDialog" @update:open="showDeleteDialog = $event">
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Note</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete this note? This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction @click="confirmDelete" class="bg-destructive text-destructive-foreground hover:bg-destructive/90">
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</template>
@@ -102,6 +119,16 @@ import { Reply, Pencil, Trash2 } from 'lucide-vue-next'
import { Button } from '@/components/ui/button'
import { Textarea } from '@/components/ui/textarea'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog'
import { taskService, type ProductionNote } from '@/services/task'
import { useAuthStore } from '@/stores/auth'
import { useToast } from '@/components/ui/toast/use-toast'
@@ -121,6 +148,7 @@ const authStore = useAuthStore()
const editing = ref(false)
const editContent = ref('')
const showDeleteDialog = ref(false)
const canEdit = computed(() => {
return authStore.user?.id === props.note.user_id || authStore.user?.is_admin
@@ -174,9 +202,11 @@ async function handleSave() {
}
}
async function handleDelete() {
if (!confirm('Are you sure you want to delete this note?')) return
function handleDelete() {
showDeleteDialog.value = true
}
async function confirmDelete() {
try {
await taskService.deleteTaskNote(props.taskId, props.note.id)
emit('noteUpdated')
@@ -185,12 +215,13 @@ async function handleDelete() {
description: 'Note deleted successfully'
})
} catch (error: any) {
console.error('Error deleting note:', error)
toast({
title: 'Error',
description: error.response?.data?.detail || 'Failed to delete note',
variant: 'destructive'
})
} finally {
showDeleteDialog.value = false
}
}
@@ -104,6 +104,23 @@
</div>
</DialogContent>
</Dialog>
<AlertDialog :open="showDeleteDialog" @update:open="showDeleteDialog = $event">
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Attachment</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete this attachment? This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction @click="confirmDeleteAttachment" class="bg-destructive text-destructive-foreground hover:bg-destructive/90">
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</template>
@@ -125,6 +142,16 @@ import {
DialogTitle,
DialogDescription,
} from '@/components/ui/dialog'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog'
import AttachmentCard from './AttachmentCard.vue'
import { taskService, type TaskAttachment } from '@/services/task'
import { useToast } from '@/components/ui/toast/use-toast'
@@ -148,6 +175,8 @@ const filterType = ref('all')
const viewerOpen = ref(false)
const selectedAttachment = ref<TaskAttachment | null>(null)
const mediaBlobUrl = ref<string | null>(null)
const showDeleteDialog = ref(false)
const attachmentToDelete = ref<number | null>(null)
const attachmentTypes = [
{ value: 'all', label: 'All' },
@@ -197,23 +226,30 @@ async function handleFileSelect(event: Event) {
}
}
async function handleDelete(attachmentId: number) {
if (!confirm('Are you sure you want to delete this attachment?')) return
function handleDelete(attachmentId: number) {
attachmentToDelete.value = attachmentId
showDeleteDialog.value = true
}
async function confirmDeleteAttachment() {
if (attachmentToDelete.value === null) return
try {
await taskService.deleteTaskAttachment(props.taskId, attachmentId)
await taskService.deleteTaskAttachment(props.taskId, attachmentToDelete.value)
emit('attachmentsUpdated')
toast({
title: 'Success',
description: 'Attachment deleted successfully'
})
} catch (error: any) {
console.error('Error deleting attachment:', error)
toast({
title: 'Error',
description: error.response?.data?.detail || 'Failed to delete attachment',
variant: 'destructive'
})
} finally {
showDeleteDialog.value = false
attachmentToDelete.value = null
}
}