124_webcalibre2/src/app/dashboard/dashboard-view.tsx

274 lines
8.7 KiB
TypeScript
Raw Normal View History

'use client';
import { useState } from "react";
2026-01-17 05:12:37 +00:00
import { styled } from '@mui/material/styles';
import {
Button,
CircularProgress,
Box,
Chip,
IconButton,
Typography,
Stack,
TextField,
InputAdornment,
2026-01-16 05:22:00 +00:00
Tooltip,
} from "@mui/material";
2026-01-12 11:53:20 +00:00
import {
DataGrid,
GridColDef,
2026-01-17 05:12:37 +00:00
Toolbar,
2026-01-16 05:22:00 +00:00
QuickFilter,
QuickFilterControl,
2026-01-17 05:12:37 +00:00
QuickFilterClear,
2026-01-12 11:53:20 +00:00
} from "@mui/x-data-grid";
import SyncIcon from "@mui/icons-material/Sync";
import RefreshIcon from "@mui/icons-material/Refresh";
import FolderIcon from "@mui/icons-material/Folder";
import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile";
import DeleteIcon from "@mui/icons-material/Delete";
import EditIcon from "@mui/icons-material/Edit";
2026-01-12 11:53:20 +00:00
import SearchIcon from '@mui/icons-material/Search';
2026-01-17 05:12:37 +00:00
import CancelIcon from '@mui/icons-material/Cancel';
import DownloadIcon from '@mui/icons-material/Download';
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
import { syncOneDrive } from "./sync-actions";
2026-01-16 05:22:00 +00:00
import { deleteFileNodeAction } from "./actions";
import { useRouter } from "next/navigation";
2026-01-17 05:12:37 +00:00
// --- 1. Styled Component for Search Placement ---
const StyledQuickFilter = styled(QuickFilter)({
marginLeft: 'auto', // Pushes the search box to the right side of the toolbar
});
// --- 2. Custom Toolbar Component ---
2026-01-12 11:53:20 +00:00
function CustomToolbar() {
return (
2026-01-17 05:12:37 +00:00
<Toolbar sx={{ p: 2, borderBottom: '1px solid', borderColor: 'divider' }}>
2026-01-12 11:53:20 +00:00
<Typography variant="h6" fontWeight="bold" color="primary">
Library
</Typography>
2026-01-17 05:12:37 +00:00
{/* The 'expanded' prop ensures the search input is always visible by default */}
<StyledQuickFilter expanded>
<QuickFilterControl
render={({ ref, ...other }) => (
<TextField
{...other}
sx={{ width: 300 }}
inputRef={ref}
placeholder="Search library..."
size="small"
slotProps={{
input: {
startAdornment: (
<InputAdornment position="start">
<SearchIcon fontSize="small" />
</InputAdornment>
),
endAdornment: other.value ? (
<InputAdornment position="end">
<QuickFilterClear
edge="end"
size="small"
material={{ sx: { marginRight: -0.75 } }}
>
<CancelIcon fontSize="small" />
</QuickFilterClear>
</InputAdornment>
) : null,
// Ensure other props are spread correctly
...other.slotProps?.input,
},
...other.slotProps,
}}
/>
)}
/>
</StyledQuickFilter>
</Toolbar>
2026-01-12 11:53:20 +00:00
);
}
2026-01-17 05:12:37 +00:00
// --- 3. Main Dashboard View ---
interface DashboardViewProps {
initialFiles: any[];
user?: {
id?: string;
role?: string;
};
}
export default function DashboardView({ initialFiles, user }: DashboardViewProps) {
const [loading, setLoading] = useState(false);
const [isRefreshing, setIsRefreshing] = useState(false);
const router = useRouter();
const isAdmin = user?.role === "ADMIN";
2026-01-18 03:02:56 +00:00
const [lastSynced, setLastSynced] = useState<Date | null>(new Date()); // Defaults to 'Just now' on load
const getVirtualPath = (parentId: string | null): string => {
if (!parentId) return "WebCalibre";
const parent = initialFiles.find((f) => f.id === parentId);
if (!parent) return "WebCalibre";
const prefix = parent.parentId ? `${getVirtualPath(parent.parentId)} / ` : "";
return `${prefix}${parent.name}`;
};
const handleSync = async () => {
2026-01-18 03:02:56 +00:00
setLoading(true);
try {
await syncOneDrive();
setLastSynced(new Date()); // Update the time
router.refresh();
} catch (error) {
console.error("Sync failed:", error);
} finally {
setLoading(false);
}
};
const handleRefresh = () => {
setIsRefreshing(true);
router.refresh();
setTimeout(() => setIsRefreshing(false), 800);
};
const handleDelete = async (id: string, name: string) => {
if (!confirm(`Are you sure you want to delete "${name}"?`)) return;
try {
2026-01-16 05:22:00 +00:00
await deleteFileNodeAction(id);
router.refresh();
} catch (error: any) {
alert(error.message || "Failed to delete file");
}
};
const columns: GridColDef[] = [
{
field: "name",
headerName: "Name",
flex: 1.5,
minWidth: 250,
renderCell: (params) => (
2026-01-16 05:22:00 +00:00
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, height: '100%' }}>
2026-01-12 11:53:20 +00:00
{params.row.isFolder ? <FolderIcon sx={{ color: '#FFB020' }} /> : <InsertDriveFileIcon color="action" />}
<Typography variant="body2">{params.value}</Typography>
</Box>
)
},
{
field: "parentId",
2026-01-12 11:53:20 +00:00
headerName: "Location",
flex: 1,
2026-01-12 11:53:20 +00:00
renderCell: (params) => <Chip label={getVirtualPath(params.value)} size="small" variant="outlined" />
},
2026-01-12 11:53:20 +00:00
{ field: "description", headerName: "Description", flex: 1 },
{
field: "type",
headerName: "Type",
2026-01-12 11:53:20 +00:00
width: 120,
valueGetter: (value, row) => row.metadata?.type || (row.isFolder ? "Folder" : "File"),
renderCell: (params) => (
2026-01-17 05:12:37 +00:00
<Typography variant="caption" sx={{ textTransform: 'uppercase', fontWeight: 'bold' }}>
{params.value}
</Typography>
)
},
{
field: "size",
headerName: "Size",
width: 100,
2026-01-12 11:53:20 +00:00
renderCell: (params) => params.row.isFolder ? "--" : `${(Number(params.value) / 1024 / 1024).toFixed(2)} MB`
},
2026-01-17 05:12:37 +00:00
{
field: "metadata_search",
headerName: "Search Metadata",
width: 0,
valueGetter: (value, row) => row.metadata ? JSON.stringify(row.metadata) : ""
},
{
field: "actions",
headerName: "Actions",
2026-01-16 05:22:00 +00:00
width: 180,
align: 'right',
renderCell: (params) => {
const isOwner = params.row.ownerId === user?.id;
const isFolder = params.row.isFolder;
return (
2026-01-16 05:22:00 +00:00
<Stack direction="row" spacing={0.5} justifyContent="flex-end" alignItems="center" sx={{ height: '100%' }}>
{!isFolder && (
<>
2026-01-17 05:12:37 +00:00
<IconButton size="small" color="info" onClick={() => window.open(`/api/download?id=${params.row.id}&mode=inline`, '_blank')}>
<OpenInNewIcon fontSize="small" />
</IconButton>
<IconButton size="small" color="success" onClick={() => window.location.href = `/api/download?id=${params.row.id}&mode=attachment`}>
<DownloadIcon fontSize="small" />
</IconButton>
</>
)}
{(isAdmin || isOwner) && (
<>
2026-01-17 05:12:37 +00:00
<IconButton size="small" color="primary" onClick={() => router.push(`/update/${params.row.id}`)}>
<EditIcon fontSize="small" />
</IconButton>
<IconButton size="small" color="error" onClick={() => handleDelete(params.row.id, params.row.name)}>
<DeleteIcon fontSize="small" />
</IconButton>
</>
)}
</Stack>
);
}
}
];
return (
2026-01-16 05:22:00 +00:00
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
2026-01-18 03:02:56 +00:00
<Box sx={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 2 }}>
{lastSynced && (
<Typography variant="caption" color="text.secondary" sx={{ fontStyle: 'italic' }}>
Last synced: {lastSynced.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</Typography>
)}
2026-01-16 05:22:00 +00:00
<Button
variant="outlined"
startIcon={isRefreshing ? <CircularProgress size={16} /> : <RefreshIcon />}
onClick={handleRefresh}
>
2026-01-17 05:12:37 +00:00
Refresh
</Button>
2026-01-16 05:22:00 +00:00
<Button
variant="contained"
startIcon={loading ? <CircularProgress size={20} color="inherit" /> : <SyncIcon />}
onClick={handleSync}
disabled={loading}
>
2026-01-12 11:53:20 +00:00
Sync OneDrive
</Button>
</Box>
2026-01-17 05:12:37 +00:00
<Box sx={{ height: 750, width: "100%", bgcolor: 'background.paper', borderRadius: 3, boxShadow: 1, overflow: 'hidden' }}>
<DataGrid
rows={initialFiles}
columns={columns}
2026-01-17 05:12:37 +00:00
// --- THE FIX: showToolbar must be true, and toolbar slot must be assigned ---
showToolbar
2026-01-12 11:53:20 +00:00
slots={{ toolbar: CustomToolbar }}
disableRowSelectionOnClick
2026-01-17 05:12:37 +00:00
initialState={{
columns: {
columnVisibilityModel: {
metadata_search: false,
},
},
}}
2026-01-17 05:12:37 +00:00
sx={{ border: 'none' }}
/>
</Box>
</Box>
);
}