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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -83,6 +83,12 @@ const routes: RouteRecordRaw[] = [
|
||||
name: 'ProjectSettings',
|
||||
component: () => import('@/views/ProjectSettingsView.vue'),
|
||||
meta: { tab: 'settings', tabLabel: 'Settings' }
|
||||
},
|
||||
{
|
||||
path: 'technical-specs',
|
||||
name: 'ProjectTechnicalSpecs',
|
||||
component: () => import('@/views/ProjectTechnicalSpecsView.vue'),
|
||||
meta: { tab: 'technical-specs', tabLabel: 'Technical Specs' }
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -150,18 +150,8 @@ class AssetService {
|
||||
}
|
||||
|
||||
const url = `/assets/?${params}`
|
||||
console.log('AssetService - Fetching assets from:', url)
|
||||
console.log('AssetService - Project ID:', projectId)
|
||||
|
||||
try {
|
||||
const response = await apiClient.get(url)
|
||||
console.log('AssetService - Assets response status:', response.status)
|
||||
console.log('AssetService - Assets response data:', response.data)
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('AssetService - Error fetching assets:', error)
|
||||
throw error
|
||||
}
|
||||
const response = await apiClient.get(url)
|
||||
return response.data
|
||||
}
|
||||
|
||||
async getAsset(assetId: number): Promise<Asset> {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { apiClient } from './api'
|
||||
|
||||
export interface DeveloperStats {
|
||||
total_projects: number
|
||||
total_tasks: number
|
||||
total_submissions: number
|
||||
api_usage_count: number
|
||||
}
|
||||
|
||||
export const developerService = {
|
||||
async getStats(): Promise<DeveloperStats> {
|
||||
const response = await apiClient.get('/developer/stats')
|
||||
return response.data
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { apiClient } from './api'
|
||||
|
||||
export interface PendingReviewSubmission {
|
||||
id: number
|
||||
task_id: number
|
||||
user_id: number
|
||||
file_name: string
|
||||
version_number: number
|
||||
submitted_at: string
|
||||
user_first_name: string
|
||||
user_last_name: string
|
||||
}
|
||||
|
||||
export const reviewService = {
|
||||
async getPendingReviews(projectId?: number): Promise<PendingReviewSubmission[]> {
|
||||
const params = projectId ? { project_id: projectId } : {}
|
||||
const response = await apiClient.get('/reviews/pending', { params })
|
||||
return response.data
|
||||
}
|
||||
}
|
||||
@@ -86,17 +86,8 @@ export const userService = {
|
||||
is_approved?: boolean
|
||||
is_admin?: boolean
|
||||
}): Promise<User> {
|
||||
console.log('userService.editUser called with:', { userId, userData })
|
||||
try {
|
||||
const response = await apiClient.put(`/users/${userId}`, userData)
|
||||
return normalizeUser(response.data)
|
||||
} catch (error: any) {
|
||||
console.error('editUser API error:', error.response?.data)
|
||||
if (error.response?.data?.detail && Array.isArray(error.response.data.detail)) {
|
||||
console.error('Validation errors:', JSON.stringify(error.response.data.detail, null, 2))
|
||||
}
|
||||
throw error
|
||||
}
|
||||
const response = await apiClient.put(`/users/${userId}`, userData)
|
||||
return normalizeUser(response.data)
|
||||
},
|
||||
|
||||
async resetUserPassword(userId: number, newPassword: string): Promise<{ message: string; user_id: number }> {
|
||||
|
||||
@@ -295,8 +295,8 @@
|
||||
<div v-for="submission in pendingReviews" :key="submission.id" class="flex items-center gap-3 p-3 rounded-lg bg-muted/50">
|
||||
<div class="h-2 w-2 rounded-full bg-orange-500"></div>
|
||||
<div class="flex-1">
|
||||
<p class="font-medium">{{ submission.task_name }}</p>
|
||||
<p class="text-sm text-muted-foreground">by {{ submission.artist_name }}</p>
|
||||
<p class="font-medium">{{ submission.file_name }}</p>
|
||||
<p class="text-sm text-muted-foreground">by {{ submission.user_first_name }} {{ submission.user_last_name }}</p>
|
||||
</div>
|
||||
<div class="text-sm text-muted-foreground">{{ formatSubmissionTime(submission.submitted_at) }}</div>
|
||||
</div>
|
||||
@@ -341,10 +341,10 @@
|
||||
<div v-for="activity in systemActivity" :key="activity.id" class="flex items-center gap-3 p-3 rounded-lg bg-muted/50">
|
||||
<div class="h-2 w-2 rounded-full bg-green-500"></div>
|
||||
<div class="flex-1">
|
||||
<p class="font-medium">{{ activity.action }}</p>
|
||||
<p class="font-medium">{{ formatActivityType(activity.type) }}</p>
|
||||
<p class="text-sm text-muted-foreground">{{ activity.description }}</p>
|
||||
</div>
|
||||
<div class="text-sm text-muted-foreground">{{ formatTime(activity.timestamp) }}</div>
|
||||
<div class="text-sm text-muted-foreground">{{ formatTime(activity.created_at) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -400,10 +400,6 @@
|
||||
<Key class="h-4 w-4 mr-2" />
|
||||
Manage API Keys
|
||||
</Button>
|
||||
<Button variant="outline" class="justify-start" @click="navigateTo('/developer/docs')">
|
||||
<FileText class="h-4 w-4 mr-2" />
|
||||
API Documentation
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<!-- Admin Actions (when user has admin permission) -->
|
||||
@@ -428,43 +424,57 @@
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import {
|
||||
CheckSquare, Clock, CheckCircle, FolderOpen, User, AlertTriangle, Users,
|
||||
TrendingUp, Eye, RotateCcw, Key, Activity, Database, UserCheck, Shield,
|
||||
FileText
|
||||
CheckSquare, Clock, CheckCircle, FolderOpen, User, AlertTriangle, Users,
|
||||
TrendingUp, Eye, RotateCcw, Key, Activity, Database, UserCheck, Shield
|
||||
} from 'lucide-vue-next'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useProjectsStore } from '@/stores/projects'
|
||||
import { useTasksStore } from '@/stores/tasks'
|
||||
import { userService } from '@/services/user'
|
||||
import { apiKeyService } from '@/services/apiKey'
|
||||
import { developerService } from '@/services/developer'
|
||||
import { reviewService, type PendingReviewSubmission } from '@/services/review'
|
||||
import { getRecentActivities } from '@/services/activity'
|
||||
import type { Activity as ActivityRecord } from '@/types/activity'
|
||||
import { ActivityType } from '@/types/activity'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const router = useRouter()
|
||||
const projectsStore = useProjectsStore()
|
||||
const tasksStore = useTasksStore()
|
||||
|
||||
const user = computed(() => authStore.user)
|
||||
const userRole = computed(() => authStore.userRole)
|
||||
const isAdmin = computed(() => authStore.isAdmin)
|
||||
|
||||
// Dashboard stats (mock data for now - would be fetched from API)
|
||||
const dashboardStats = ref({
|
||||
// Artist stats
|
||||
// TODO(backend): "my tasks/projects across all projects" has no dedicated endpoint yet
|
||||
activeTasks: 0,
|
||||
pendingReviews: 0,
|
||||
completedTasks: 0,
|
||||
myProjects: 0,
|
||||
|
||||
|
||||
// Coordinator stats
|
||||
totalProjects: 0,
|
||||
overdueTasks: 0,
|
||||
// TODO(backend): no endpoint for role-filtered active-artist counts
|
||||
activeArtists: 0,
|
||||
// TODO(backend): Project has no completion_rate field
|
||||
completionRate: 0,
|
||||
|
||||
|
||||
// Director stats
|
||||
// TODO(backend): no "approved/retakes today" aggregate endpoint
|
||||
approvedToday: 0,
|
||||
retakesRequested: 0,
|
||||
|
||||
|
||||
// Developer stats
|
||||
apiKeys: 0,
|
||||
// TODO(backend): /developer/stats' api_usage_count is all-time, not "today"
|
||||
apiCallsToday: 0,
|
||||
totalTasks: 0,
|
||||
|
||||
|
||||
// Admin stats
|
||||
totalUsers: 0,
|
||||
pendingApprovals: 0,
|
||||
@@ -473,23 +483,12 @@ const dashboardStats = ref({
|
||||
|
||||
// Role-specific data
|
||||
const recentTasks = ref([])
|
||||
// TODO(backend): project completion_rate isn't available yet, so this list is left empty for now
|
||||
const projectOverview = ref([])
|
||||
const pendingReviews = ref([])
|
||||
const pendingReviews = ref<PendingReviewSubmission[]>([])
|
||||
// TODO(backend): needs a frontend wrapper for GET /developer/api-usage plus per-day filtering
|
||||
const apiActivity = ref([])
|
||||
const systemActivity = ref([
|
||||
{
|
||||
id: 1,
|
||||
action: 'User Registration',
|
||||
description: 'New user registered and awaiting approval',
|
||||
timestamp: new Date().toISOString()
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
action: 'Project Created',
|
||||
description: 'New project "Animation Series" created',
|
||||
timestamp: new Date(Date.now() - 3600000).toISOString()
|
||||
}
|
||||
])
|
||||
const systemActivity = ref<ActivityRecord[]>([])
|
||||
|
||||
// Methods
|
||||
const formatRole = (role?: string) => {
|
||||
@@ -535,6 +534,13 @@ const formatTime = (timestamp: string) => {
|
||||
return `${diffDays}d ago`
|
||||
}
|
||||
|
||||
const formatActivityType = (type: ActivityType) => {
|
||||
return type
|
||||
.split('_')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
const getTaskStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'not_started': return 'bg-gray-500'
|
||||
@@ -562,26 +568,59 @@ const navigateTo = (path: string) => {
|
||||
}
|
||||
|
||||
const loadDashboardData = async () => {
|
||||
// Mock data loading - in real implementation, this would fetch from API based on user role
|
||||
try {
|
||||
// Load role-specific stats
|
||||
dashboardStats.value = {
|
||||
activeTasks: 5,
|
||||
pendingReviews: 2,
|
||||
completedTasks: 28,
|
||||
myProjects: 3,
|
||||
totalProjects: 8,
|
||||
overdueTasks: 3,
|
||||
activeArtists: 12,
|
||||
completionRate: 75,
|
||||
approvedToday: 8,
|
||||
retakesRequested: 2,
|
||||
apiKeys: 3,
|
||||
apiCallsToday: 156,
|
||||
totalTasks: 245,
|
||||
totalUsers: 25,
|
||||
pendingApprovals: 2,
|
||||
activeProjects: 5
|
||||
const needsProjects = userRole.value === 'coordinator' || userRole.value === 'director' || isAdmin.value
|
||||
const tasks: Promise<unknown>[] = []
|
||||
|
||||
if (needsProjects) {
|
||||
tasks.push(projectsStore.fetchProjects())
|
||||
}
|
||||
if (userRole.value === 'coordinator') {
|
||||
tasks.push(tasksStore.fetchTasks())
|
||||
}
|
||||
if (userRole.value === 'director') {
|
||||
tasks.push(
|
||||
reviewService.getPendingReviews().then(submissions => {
|
||||
pendingReviews.value = submissions
|
||||
})
|
||||
)
|
||||
}
|
||||
if (userRole.value === 'developer') {
|
||||
tasks.push(
|
||||
apiKeyService.getAPIKeys().then(keys => {
|
||||
dashboardStats.value.apiKeys = keys.length
|
||||
}),
|
||||
developerService.getStats().then(stats => {
|
||||
dashboardStats.value.totalProjects = stats.total_projects
|
||||
dashboardStats.value.totalTasks = stats.total_tasks
|
||||
})
|
||||
)
|
||||
}
|
||||
if (isAdmin.value) {
|
||||
tasks.push(
|
||||
userService.getUsers().then(users => {
|
||||
dashboardStats.value.totalUsers = users.length
|
||||
}),
|
||||
userService.getPendingUsers().then(users => {
|
||||
dashboardStats.value.pendingApprovals = users.length
|
||||
}),
|
||||
getRecentActivities().then(activities => {
|
||||
systemActivity.value = activities
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
await Promise.all(tasks)
|
||||
|
||||
if (needsProjects) {
|
||||
dashboardStats.value.totalProjects = projectsStore.projects.length
|
||||
dashboardStats.value.activeProjects = projectsStore.projectsInProgress.length
|
||||
}
|
||||
if (userRole.value === 'coordinator') {
|
||||
dashboardStats.value.overdueTasks = tasksStore.overdueTasks.length
|
||||
}
|
||||
if (userRole.value === 'director') {
|
||||
dashboardStats.value.pendingReviews = pendingReviews.value.length
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load dashboard data:', error)
|
||||
|
||||
@@ -257,15 +257,11 @@ const handleEditUser = (user: User) => {
|
||||
const handleEditUserSubmit = async (userId: number, data: UserEditData) => {
|
||||
try {
|
||||
isEditingUser.value = true
|
||||
console.log('Editing user with data:', data)
|
||||
await userStore.editUser(userId, data)
|
||||
showEditDialog.value = false
|
||||
showSuccessMessage('User updated successfully')
|
||||
await refreshData()
|
||||
} catch (err: any) {
|
||||
console.error('Failed to edit user:', err)
|
||||
console.error('Error response:', err.response?.data)
|
||||
|
||||
// Handle FastAPI validation errors (422)
|
||||
let errorMessage = 'Failed to update user'
|
||||
if (err.response?.data?.detail) {
|
||||
@@ -276,7 +272,6 @@ const handleEditUserSubmit = async (userId: number, data: UserEditData) => {
|
||||
return `${field}: ${e.msg}`
|
||||
}).join(', ')
|
||||
errorMessage = errors
|
||||
console.error('Validation errors:', err.response.data.detail)
|
||||
} else {
|
||||
errorMessage = err.response.data.detail
|
||||
}
|
||||
|
||||
@@ -915,26 +915,18 @@ const loadDeletedItems = async () => {
|
||||
|
||||
const projectId = selectedProjectId.value && selectedProjectId.value !== 'all' ? parseInt(selectedProjectId.value) : undefined
|
||||
|
||||
console.log('Loading recovery data with projectId:', projectId)
|
||||
|
||||
const [shots, assets] = await Promise.all([
|
||||
recoveryService.getDeletedShots(projectId),
|
||||
recoveryService.getDeletedAssets(projectId)
|
||||
])
|
||||
|
||||
console.log('Loaded shots for recovery:', shots.length)
|
||||
console.log('Loaded assets for recovery:', assets.length)
|
||||
console.log('Shots data:', shots)
|
||||
|
||||
deletedShots.value = shots
|
||||
deletedAssets.value = assets
|
||||
|
||||
|
||||
// Clear selections when data changes
|
||||
selectedItems.value = []
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.detail || 'Failed to load recovery data'
|
||||
console.error('Failed to load recovery data:', err)
|
||||
console.error('Error details:', err.response)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
@@ -43,16 +43,6 @@
|
||||
Login
|
||||
</Button>
|
||||
|
||||
<Button variant="outline" class="w-full" type="button" @click="handleGoogleLogin">
|
||||
<svg class="mr-2 h-4 w-4" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
|
||||
<path fill="currentColor" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
|
||||
<path fill="currentColor" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
|
||||
<path fill="currentColor" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
|
||||
</svg>
|
||||
Login with Google
|
||||
</Button>
|
||||
|
||||
<div v-if="error" class="text-sm text-destructive text-center">
|
||||
{{ error }}
|
||||
</div>
|
||||
@@ -103,8 +93,4 @@ const handleSubmit = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleGoogleLogin = () => {
|
||||
// TODO: Implement Google OAuth login
|
||||
console.log('Google login not implemented yet')
|
||||
}
|
||||
</script>
|
||||
@@ -73,16 +73,6 @@
|
||||
Create account
|
||||
</Button>
|
||||
|
||||
<Button variant="outline" class="w-full" type="button" @click="handleGoogleSignup">
|
||||
<svg class="mr-2 h-4 w-4" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
|
||||
<path fill="currentColor" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
|
||||
<path fill="currentColor" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
|
||||
<path fill="currentColor" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
|
||||
</svg>
|
||||
Sign up with Google
|
||||
</Button>
|
||||
|
||||
<div v-if="error" class="text-sm text-destructive text-center">
|
||||
{{ error }}
|
||||
</div>
|
||||
@@ -217,8 +207,4 @@ const handleSubmit = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleGoogleSignup = () => {
|
||||
// TODO: Implement Google OAuth signup
|
||||
console.log('Google signup not implemented yet')
|
||||
}
|
||||
</script>
|
||||
@@ -11,8 +11,6 @@
|
||||
:icon="Key"
|
||||
title="No API keys"
|
||||
description="Create API keys to integrate external applications with the VFX system."
|
||||
action-text="Create API Key"
|
||||
@action="() => {}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -37,19 +37,8 @@ const assetsStore = useAssetsStore()
|
||||
// Computed properties
|
||||
const projectId = computed(() => {
|
||||
const id = route.params.projectId
|
||||
const parsedId = typeof id === 'string' ? parseInt(id) : Array.isArray(id) ? parseInt(id[0]) : 0
|
||||
console.log('ProjectAssetsView - Route params:', route.params)
|
||||
console.log('ProjectAssetsView - Raw project ID:', id)
|
||||
console.log('ProjectAssetsView - Parsed project ID:', parsedId)
|
||||
return parsedId
|
||||
return typeof id === 'string' ? parseInt(id) : Array.isArray(id) ? parseInt(id[0]) : 0
|
||||
})
|
||||
|
||||
const totalAssets = computed(() => {
|
||||
console.log('ProjectAssetsView - Total assets:', assetsStore.assets.length)
|
||||
return assetsStore.assets.length
|
||||
})
|
||||
|
||||
// Debug logging on mount
|
||||
console.log('ProjectAssetsView - Component mounted')
|
||||
console.log('ProjectAssetsView - Route params:', route.params)
|
||||
const totalAssets = computed(() => assetsStore.assets.length)
|
||||
</script>
|
||||
@@ -105,29 +105,22 @@
|
||||
</div>
|
||||
|
||||
<!-- Recent Activity -->
|
||||
<Card class="mt-6">
|
||||
<CardHeader>
|
||||
<CardTitle>Recent Activity</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="text-center py-8 text-muted-foreground">
|
||||
<Activity class="h-8 w-8 mx-auto mb-2" />
|
||||
<p>Activity feed coming soon</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div class="mt-6" v-if="project">
|
||||
<ActivityFeed :project-id="project.id" title="Recent Activity" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { Camera, Package, Users, Activity } from 'lucide-vue-next'
|
||||
import { Camera, Package, Users } from 'lucide-vue-next'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { useProjectsStore } from '@/stores/projects'
|
||||
import TechnicalSpecsSummary from '@/components/project/TechnicalSpecsSummary.vue'
|
||||
import ActivityFeed from '@/components/activity/ActivityFeed.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const projectsStore = useProjectsStore()
|
||||
|
||||
Reference in New Issue
Block a user