diff --git a/next.config.ts b/next.config.ts index ef9f9af1..9b1d3963 100644 --- a/next.config.ts +++ b/next.config.ts @@ -72,6 +72,20 @@ const nextConfig: NextConfig = { // process.cwd() requires the file to be traced explicitly. '/api/v1/document-templates/**': ['./assets/eoi-template.pdf'], }, + async redirects() { + return [ + { + source: '/:portSlug/documents/files', + destination: '/:portSlug/documents', + permanent: true, + }, + { + source: '/:portSlug/documents/files/:path*', + destination: '/:portSlug/documents', + permanent: true, + }, + ]; + }, async headers() { return [ { diff --git a/src/app/(dashboard)/[portSlug]/documents/files/page.tsx b/src/app/(dashboard)/[portSlug]/documents/files/page.tsx deleted file mode 100644 index 182e8dfb..00000000 --- a/src/app/(dashboard)/[portSlug]/documents/files/page.tsx +++ /dev/null @@ -1,138 +0,0 @@ -'use client'; - -import { useState } from 'react'; -import { Grid, List, Upload } from 'lucide-react'; -import { useQueryClient } from '@tanstack/react-query'; - -import { Button } from '@/components/ui/button'; -import { PageHeader } from '@/components/shared/page-header'; -import { PermissionGate } from '@/components/shared/permission-gate'; -import { FileGrid } from '@/components/files/file-grid'; -import { FolderTree } from '@/components/files/folder-tree'; -import { FileUploadZone } from '@/components/files/file-upload-zone'; -import { FilePreviewDialog } from '@/components/files/file-preview-dialog'; -import { usePaginatedQuery } from '@/hooks/use-paginated-query'; -import { useRealtimeInvalidation } from '@/hooks/use-realtime-invalidation'; -import { useFileBrowserStore } from '@/stores/file-browser-store'; -import { apiFetch } from '@/lib/api/client'; -import type { FileRow } from '@/components/files/file-grid'; - -export default function DocumentsPage() { - const queryClient = useQueryClient(); - - const { viewMode, setViewMode, currentFolder, setCurrentFolder } = useFileBrowserStore(); - const [showUpload, setShowUpload] = useState(false); - const [previewFile, setPreviewFile] = useState(null); - const [, setRenameFile] = useState(null); - - const { data, isLoading } = usePaginatedQuery({ - queryKey: ['files'], - endpoint: '/api/v1/files', - filterDefinitions: [], - }); - - useRealtimeInvalidation({ - 'file:uploaded': [['files']], - 'file:updated': [['files']], - 'file:deleted': [['files']], - }); - - const filesInFolder = currentFolder - ? data.filter((f) => f.storagePath?.includes(currentFolder)) - : data; - - const handleDownload = async (file: FileRow) => { - try { - const res = await apiFetch<{ data: { url: string; filename: string } }>( - `/api/v1/files/${file.id}/download`, - ); - const a = document.createElement('a'); - a.href = res.data.url; - a.download = res.data.filename; - a.click(); - } catch { - // silent - } - }; - - const handleDelete = async (file: FileRow) => { - if (!confirm(`Delete "${file.filename}"? This cannot be undone.`)) return; - try { - await apiFetch(`/api/v1/files/${file.id}`, { method: 'DELETE' }); - queryClient.invalidateQueries({ queryKey: ['files'] }); - } catch { - // silent - } - }; - - return ( -
- - - - - -
- } - /> - - {showUpload && ( - - { - queryClient.invalidateQueries({ queryKey: ['files'] }); - setShowUpload(false); - }} - /> - - )} - -
- {/* Folder tree sidebar */} - - - {/* Main content */} -
- -
-
- - !open && setPreviewFile(null)} - fileId={previewFile?.id} - fileName={previewFile?.filename} - mimeType={previewFile?.mimeType ?? undefined} - /> - - ); -} diff --git a/src/components/files/folder-tree.tsx b/src/components/files/folder-tree.tsx deleted file mode 100644 index 4f571c8e..00000000 --- a/src/components/files/folder-tree.tsx +++ /dev/null @@ -1,139 +0,0 @@ -'use client'; - -import { useState } from 'react'; -import { ChevronDown, ChevronRight, Folder, FolderOpen } from 'lucide-react'; - -import { cn } from '@/lib/utils'; -import type { FileRow } from '@/components/files/file-grid'; - -interface FolderNode { - name: string; - fullPath: string; - children: Record; -} - -function buildFolderTree(files: FileRow[]): FolderNode { - const root: FolderNode = { name: '', fullPath: '', children: {} }; - - for (const file of files) { - const parts = file.storagePath ? file.storagePath.split('/').slice(0, -1) : []; - if (parts.length <= 1) continue; // skip files directly in root/port folder - - let node = root; - let accumulated = ''; - for (const part of parts.slice(1)) { // skip portSlug prefix - accumulated = accumulated ? `${accumulated}/${part}` : part; - if (!node.children[part]) { - node.children[part] = { name: part, fullPath: accumulated, children: {} }; - } - node = node.children[part]!; - } - } - - return root; -} - -interface FolderNodeComponentProps { - node: FolderNode; - currentFolder: string; - onFolderSelect: (path: string) => void; - depth?: number; -} - -function FolderNodeComponent({ - node, - currentFolder, - onFolderSelect, - depth = 0, -}: FolderNodeComponentProps) { - const [expanded, setExpanded] = useState(true); - const hasChildren = Object.keys(node.children).length > 0; - const isSelected = currentFolder === node.fullPath; - - return ( -
- - - {hasChildren && expanded && ( -
- {Object.values(node.children).map((child) => ( - - ))} -
- )} -
- ); -} - -interface FolderTreeProps { - files: (FileRow & { storagePath: string })[]; - currentFolder: string; - onFolderSelect: (path: string) => void; -} - -export function FolderTree({ files, currentFolder, onFolderSelect }: FolderTreeProps) { - const tree = buildFolderTree(files); - - return ( -
- - - {Object.values(tree.children).map((child) => ( - - ))} -
- ); -} diff --git a/src/components/layout/sidebar.tsx b/src/components/layout/sidebar.tsx index ba1296bb..62a3e736 100644 --- a/src/components/layout/sidebar.tsx +++ b/src/components/layout/sidebar.tsx @@ -13,7 +13,6 @@ import { Building2, Receipt, FileText, - FolderOpen, Bell, Camera, Globe, @@ -116,10 +115,7 @@ function buildNavSections(portSlug: string | undefined): NavSection[] { { title: 'Documents', marinaRequired: true, - items: [ - { href: `${base}/documents`, label: 'Documents', icon: FileText }, - { href: `${base}/documents/files`, label: 'Files', icon: FolderOpen }, - ], + items: [{ href: `${base}/documents`, label: 'Documents', icon: FileText }], }, { title: 'Financial', diff --git a/src/components/search/command-search.tsx b/src/components/search/command-search.tsx index adccefc3..1e6eabff 100644 --- a/src/components/search/command-search.tsx +++ b/src/components/search/command-search.tsx @@ -1010,7 +1010,7 @@ function buildFlatRows(args: BuildFlatRowsArgs): FlatRow[] { icon: Folder, label: f.filename, sub: f.ownerLabel, - href: `/${portSlug}/documents/files`, + href: `/${portSlug}/documents`, }); } } diff --git a/src/stores/file-browser-store.ts b/src/stores/file-browser-store.ts index 4b234ccd..3320f8ba 100644 --- a/src/stores/file-browser-store.ts +++ b/src/stores/file-browser-store.ts @@ -1,26 +1,15 @@ import { create } from 'zustand'; -interface FileBrowserStore { +interface FileBrowserState { viewMode: 'grid' | 'list'; - currentFolder: string; - selectedFiles: string[]; setViewMode: (mode: 'grid' | 'list') => void; - setCurrentFolder: (folder: string) => void; - toggleFileSelection: (fileId: string) => void; - clearSelection: () => void; + selectedFolderId: string | null | undefined; // undefined = no selection, null = root + setSelectedFolderId: (id: string | null | undefined) => void; } -export const useFileBrowserStore = create((set) => ({ - viewMode: 'grid', - currentFolder: '', - selectedFiles: [], - setViewMode: (mode) => set({ viewMode: mode }), - setCurrentFolder: (folder) => set({ currentFolder: folder, selectedFiles: [] }), - toggleFileSelection: (fileId) => - set((state) => ({ - selectedFiles: state.selectedFiles.includes(fileId) - ? state.selectedFiles.filter((id) => id !== fileId) - : [...state.selectedFiles, fileId], - })), - clearSelection: () => set({ selectedFiles: [] }), +export const useFileBrowserStore = create((set) => ({ + viewMode: 'list', + setViewMode: (viewMode) => set({ viewMode }), + selectedFolderId: undefined, + setSelectedFolderId: (selectedFolderId) => set({ selectedFolderId }), }));