admin panel redisign

This commit is contained in:
Dimitar765 2024-10-28 23:52:47 +01:00
parent 8cbdde39c1
commit 1804bdbb99
7 changed files with 476 additions and 210 deletions

View File

@ -46,4 +46,6 @@ model Notification {
userId Int
user User @relation(fields: [userId], references: [id])
createdAt DateTime @default(now())
}
}

View File

@ -1,4 +1,4 @@
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { S3Service } from '../s3/s3.service';
import { CreateDocumentDto } from '../dto/create-document.dto';
@ -85,7 +85,6 @@ export class AdminService {
});
}
async getAllDocuments() {
return this.prisma.document.findMany();
}
@ -172,22 +171,91 @@ export class AdminService {
});
}
// async uploadDocument(
// file: Express.Multer.File,
// title: string,
// userId: number,
// ) {
// try {
// // First create document with pending status
// const document = await this.prisma.document.create({
// data: {
// title,
// authorId: userId,
// status: 'pending',
// s3Key: '', // Temporary empty key
// },
// });
// // Update status to uploading
// await this.prisma.document.update({
// where: { id: document.id },
// data: { status: 'uploading' },
// });
// // Upload to S3
// const s3Key = await this.s3Service.uploadFile(file, 'documents');
// // Update document with s3Key and completed status
// return this.prisma.document.update({
// where: { id: document.id },
// data: {
// s3Key,
// status: 'completed',
// },
// });
// } catch (error) {
// // If anything fails, update status to failed
// const document = await this.prisma.document.findFirst({
// where: { title, authorId: userId },
// });
// if (document) {
// await this.prisma.document.update({
// where: { id: document.id },
// data: { status: 'failed' },
// });
// }
// throw error;
// }
// }
// problem whith upload status writing to db, i will fix it later
async uploadDocument(
file: Express.Multer.File,
title: string,
authorId: number,
userId: number,
) {
const key = `documents/${Date.now()}-${file.originalname}`;
const s3Key = await this.s3Service.uploadFile(file, key);
let s3Key;
try {
// First upload to S3
s3Key = await this.s3Service.uploadFile(file, 'documents');
return this.prisma.document.create({
data: {
title,
s3Key,
authorId,
status: 'completed',
},
});
// Then create document with completed status and s3Key
const document = await this.prisma.document.create({
data: {
title,
authorId: userId,
s3Key,
status: 'completed', // Set status to completed immediately after successful upload
},
});
return document;
} catch (error) {
// Create document with failed status if upload fails
if (title && userId) {
await this.prisma.document.create({
data: {
title,
authorId: userId,
s3Key: s3Key || '',
status: 'failed',
},
});
}
throw error;
}
}
async getDocumentUrl(documentId: number) {

View File

@ -20,13 +20,13 @@
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@vitejs/plugin-react": "^4.0.3",
"autoprefixer": "^10.4.15",
"autoprefixer": "^10.4.20",
"eslint": "^8.45.0",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.3",
"postcss": "^8.4.28",
"tailwindcss": "^3.3.3",
"postcss": "^8.4.47",
"tailwindcss": "^3.4.14",
"vite": "^4.4.5"
}
},
@ -1166,9 +1166,9 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/autoprefixer": {
"version": "10.4.15",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.15.tgz",
"integrity": "sha512-KCuPB8ZCIqFdA4HwKXsvz7j6gvSDNhDP7WnUjBleRkKjPdvCmHFuQ77ocavI8FT6NdvlBnE2UFr2H4Mycn8Vew==",
"version": "10.4.20",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
"integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==",
"dev": true,
"funding": [
{
@ -1185,11 +1185,11 @@
}
],
"dependencies": {
"browserslist": "^4.21.10",
"caniuse-lite": "^1.0.30001520",
"fraction.js": "^4.2.0",
"browserslist": "^4.23.3",
"caniuse-lite": "^1.0.30001646",
"fraction.js": "^4.3.7",
"normalize-range": "^0.1.2",
"picocolors": "^1.0.0",
"picocolors": "^1.0.1",
"postcss-value-parser": "^4.2.0"
},
"bin": {
@ -1262,9 +1262,9 @@
}
},
"node_modules/browserslist": {
"version": "4.21.10",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz",
"integrity": "sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==",
"version": "4.24.2",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz",
"integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==",
"dev": true,
"funding": [
{
@ -1281,10 +1281,10 @@
}
],
"dependencies": {
"caniuse-lite": "^1.0.30001517",
"electron-to-chromium": "^1.4.477",
"node-releases": "^2.0.13",
"update-browserslist-db": "^1.0.11"
"caniuse-lite": "^1.0.30001669",
"electron-to-chromium": "^1.5.41",
"node-releases": "^2.0.18",
"update-browserslist-db": "^1.1.1"
},
"bin": {
"browserslist": "cli.js"
@ -1325,9 +1325,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001523",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001523.tgz",
"integrity": "sha512-I5q5cisATTPZ1mc588Z//pj/Ox80ERYDfR71YnvY7raS/NOk8xXlZcB0sF7JdqaV//kOaa6aus7lRfpdnt1eBA==",
"version": "1.0.30001673",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001673.tgz",
"integrity": "sha512-WTrjUCSMp3LYX0nE12ECkV0a+e6LC85E0Auz75555/qr78Oc8YWhEPNfDd6SHdtlCMSzqtuXY0uyEMNRcsKpKw==",
"dev": true,
"funding": [
{
@ -1524,9 +1524,9 @@
}
},
"node_modules/electron-to-chromium": {
"version": "1.4.503",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.503.tgz",
"integrity": "sha512-LF2IQit4B0VrUHFeQkWhZm97KuJSGF2WJqq1InpY+ECpFRkXd8yTIaTtJxsO0OKDmiBYwWqcrNaXOurn2T2wiA==",
"version": "1.5.48",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.48.tgz",
"integrity": "sha512-FXULnNK7ACNI9MTMOVAzUGiz/YrK9Kcb0s/JT4aJgsam7Eh6XYe7Y6q95lPq+VdBe1DpT2eTnfXFtnuPGCks4w==",
"dev": true
},
"node_modules/es-abstract": {
@ -1682,9 +1682,9 @@
}
},
"node_modules/escalade": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
"integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"dev": true,
"engines": {
"node": ">=6"
@ -2148,16 +2148,16 @@
}
},
"node_modules/fraction.js": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.1.tgz",
"integrity": "sha512-/KxoyCnPM0GwYI4NN0Iag38Tqt+od3/mLuguepLgCAKPn0ZhC544nssAW0tG2/00zXEYl9W+7hwAIpLHo6Oc7Q==",
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
"integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==",
"dev": true,
"engines": {
"node": "*"
},
"funding": {
"type": "patreon",
"url": "https://www.patreon.com/infusion"
"url": "https://github.com/sponsors/rawify"
}
},
"node_modules/fs.realpath": {
@ -2815,9 +2815,9 @@
}
},
"node_modules/jiti": {
"version": "1.19.3",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.19.3.tgz",
"integrity": "sha512-5eEbBDQT/jF1xg6l36P+mWGGoH9Spuy0PCdSr2dtWRDGC6ph/w9ZCL4lmESW8f8F7MwT3XKescfP0wnZWAKL9w==",
"version": "1.21.6",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz",
"integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==",
"dev": true,
"bin": {
"jiti": "bin/jiti.js"
@ -3078,9 +3078,9 @@
"dev": true
},
"node_modules/node-releases": {
"version": "2.0.13",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz",
"integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==",
"version": "2.0.18",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz",
"integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==",
"dev": true
},
"node_modules/normalize-path": {
@ -4007,9 +4007,9 @@
}
},
"node_modules/tailwindcss": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.3.tgz",
"integrity": "sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==",
"version": "3.4.14",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.14.tgz",
"integrity": "sha512-IcSvOcTRcUtQQ7ILQL5quRDg7Xs93PdJEk1ZLbhhvJc7uj/OAhYOnruEiwnGgBvUtaUAJ8/mhSw1o8L2jCiENA==",
"dev": true,
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
@ -4017,10 +4017,10 @@
"chokidar": "^3.5.3",
"didyoumean": "^1.2.2",
"dlv": "^1.1.3",
"fast-glob": "^3.2.12",
"fast-glob": "^3.3.0",
"glob-parent": "^6.0.2",
"is-glob": "^4.0.3",
"jiti": "^1.18.2",
"jiti": "^1.21.0",
"lilconfig": "^2.1.0",
"micromatch": "^4.0.5",
"normalize-path": "^3.0.0",
@ -4210,9 +4210,9 @@
}
},
"node_modules/update-browserslist-db": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz",
"integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==",
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz",
"integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==",
"dev": true,
"funding": [
{
@ -4229,8 +4229,8 @@
}
],
"dependencies": {
"escalade": "^3.1.1",
"picocolors": "^1.0.0"
"escalade": "^3.2.0",
"picocolors": "^1.1.0"
},
"bin": {
"update-browserslist-db": "cli.js"

View File

@ -22,13 +22,13 @@
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@vitejs/plugin-react": "^4.0.3",
"autoprefixer": "^10.4.15",
"autoprefixer": "^10.4.20",
"eslint": "^8.45.0",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.3",
"postcss": "^8.4.28",
"tailwindcss": "^3.3.3",
"postcss": "^8.4.47",
"tailwindcss": "^3.4.14",
"vite": "^4.4.5"
}
}

View File

@ -1,164 +1,238 @@
import { useState, useEffect } from 'react';
import { getAllUsers, uploadDocument } from '../../services/api';
function AdminPanel() {
const [users, setUsers] = useState([]);
const [file, setFile] = useState(null);
const [title, setTitle] = useState('');
useEffect(() => {
fetchUsers();
}, []);
const fetchUsers = async () => {
try {
const response = await getAllUsers();
setUsers(response.data);
} catch (error) {
console.error('Failed to fetch users:', error);
}
};
const handleFileUpload = async (e) => {
e.preventDefault();
if (!file) return;
const formData = new FormData();
formData.append('file', file);
formData.append('title', title);
try {
await uploadDocument(formData);
alert('Document uploaded successfully');
} catch (error) {
console.error('Failed to upload document:', error);
}
};
return (
<div>
<h2>Users</h2>
<ul>
{users.map(user => (
<li key={user.id}>{user.name} ({user.email})</li>
))}
</ul>
<h2>Upload Document</h2>
<form onSubmit={handleFileUpload}>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Document Title"
required
/>
<input
type="file"
onChange={(e) => setFile(e.target.files[0])}
required
/>
<button type="submit">Upload</button>
</form>
</div>
);
}
export default AdminPanel;
// import { useState } from 'react';
// import { uploadDocument } from '../../services/api';
// const AdminPanel = () => {
// import { useState, useEffect } from 'react';
// import { getAllUsers, uploadDocument } from '../../services/api';
// import DocumentUpload from '../documentUpload/DocumentUpload';
// function AdminPanel() {
// const [users, setUsers] = useState([]);
// const [file, setFile] = useState(null);
// const [progress, setProgress] = useState(0);
// const [title, setTitle] = useState('');
// const [content, setContent] = useState('');
// const [isUploading, setIsUploading] = useState(false);
// const handleFileChange = (event) => {
// if (event.target.files) {
// setFile(event.target.files[0]);
// useEffect(() => {
// fetchUsers();
// }, []);
// const fetchUsers = async () => {
// try {
// const response = await getAllUsers();
// setUsers(response.data);
// } catch (error) {
// console.error('Failed to fetch users:', error);
// }
// };
// const handleSubmit = async (event) => {
// event.preventDefault();
// if (!file || !title) {
// alert('Please select a file and enter a title');
// return;
// }
// const handleFileUpload = async (e) => {
// e.preventDefault();
// if (!file) return;
// const formData = new FormData();
// formData.append('file', file);
// formData.append('title', title);
// formData.append('content', content);
// setIsUploading(true);
// try {
// await uploadDocument(formData, (progressEvent) => {
// const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total);
// setProgress(percentCompleted);
// });
// alert('Document uploaded successfully!');
// setProgress(0);
// setFile(null);
// setTitle('');
// setContent('');
// await uploadDocument(formData);
// alert('Document uploaded successfully');
// } catch (error) {
// console.error('Upload failed:', error);
// alert('Upload failed. Please try again.');
// } finally {
// setIsUploading(false);
// setProgress(0);
// console.error('Failed to upload document:', error);
// }
// };
// return (
// <div className="admin-panel">
// <h2>Admin Panel - Upload Document</h2>
// <form onSubmit={handleSubmit}>
// <div>
// <label htmlFor="title">Document Title:</label>
// <input
// type="text"
// id="title"
// value={title}
// onChange={(e) => setTitle(e.target.value)}
// required
// />
// </div>
// <div>
// <label htmlFor="content">Document Content:</label>
// <textarea
// id="content"
// value={content}
// onChange={(e) => setContent(e.target.value)}
// rows="4"
// />
// </div>
// <div>
// <label htmlFor="file">Select File:</label>
// <input
// type="file"
// id="file"
// onChange={handleFileChange}
// required
// />
// </div>
// <button type="submit" disabled={isUploading}>
// {isUploading ? 'Uploading...' : 'Upload Document'}
// </button>
// {progress > 0 && (
// <div>
// <progress value={progress} max="100" />
// <p>{progress}% uploaded</p>
// </div>
// )}
// <div>
// <h2>Users</h2>
// <ul>
// {users.map(user => (
// <li key={user.id}>{user.name} ({user.email})</li>
// ))}
// </ul>
// <h2>Upload Document</h2>
// <form onSubmit={handleFileUpload}>
// <input
// type="text"
// value={title}
// onChange={(e) => setTitle(e.target.value)}
// placeholder="Document Title"
// required
// />
// <input
// type="file"
// onChange={(e) => setFile(e.target.files[0])}
// required
// />
// <button type="submit">Upload</button>
// </form>
// <DocumentUpload />
// </div>
// );
// };
// }
// export default AdminPanel;
// export default AdminPanel;
import { useState, useEffect } from 'react';
import { getAllUsers, getAllDocuments } from '../../services/api';
import DocumentUpload from '../documentUpload/DocumentUpload';
function AdminPanel() {
const [activeTab, setActiveTab] = useState('documents');
const [users, setUsers] = useState([]);
const [documents, setDocuments] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
useEffect(() => {
fetchData();
}, [activeTab]);
const fetchData = async () => {
setLoading(true);
setError('');
try {
if (activeTab === 'users') {
const response = await getAllUsers();
setUsers(response.data);
} else if (activeTab === 'documents') {
const response = await getAllDocuments();
setDocuments(response.data);
}
} catch (err) {
setError('Failed to fetch data. Please try again.');
} finally {
setLoading(false);
}
};
const getStatusColor = (status) => {
const colors = {
pending: 'bg-yellow-100 text-yellow-800',
uploading: 'bg-blue-100 text-blue-800',
completed: 'bg-green-100 text-green-800',
failed: 'bg-red-100 text-red-800'
};
return colors[status] || 'bg-gray-100 text-gray-800';
};
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="bg-white rounded-lg shadow">
{/* Admin Header */}
<div className="px-6 py-4 border-b border-gray-200">
<h1 className="text-2xl font-bold text-gray-900">Admin Dashboard</h1>
</div>
{/* Tab Navigation */}
<div className="border-b border-gray-200">
<nav className="flex -mb-px">
<button
onClick={() => setActiveTab('documents')}
className={`px-6 py-3 border-b-2 font-medium text-sm ${
activeTab === 'documents'
? 'border-indigo-500 text-indigo-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
Documents
</button>
<button
onClick={() => setActiveTab('users')}
className={`px-6 py-3 border-b-2 font-medium text-sm ${
activeTab === 'users'
? 'border-indigo-500 text-indigo-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
Users
</button>
<button
onClick={() => setActiveTab('upload')}
className={`px-6 py-3 border-b-2 font-medium text-sm ${
activeTab === 'upload'
? 'border-indigo-500 text-indigo-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
Upload Document
</button>
</nav>
</div>
{/* Content Area */}
<div className="p-6">
{error && (
<div className="mb-4 p-4 bg-red-100 text-red-700 rounded-md">
{error}
</div>
)}
{loading ? (
<div className="flex justify-center items-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-indigo-600"></div>
</div>
) : (
<>
{activeTab === 'documents' && (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Title</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Author</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Created At</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{documents.map((doc) => (
<tr key={doc.id}>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{doc.title}</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${getStatusColor(doc.status)}`}>
{doc.status}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{doc.authorId}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{new Date(doc.createdAt).toLocaleDateString()}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{activeTab === 'users' && (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Email</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Role</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{users.map((user) => (
<tr key={user.id}>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{user.name}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{user.email}</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${user.isAdmin ? 'bg-purple-100 text-purple-800' : 'bg-gray-100 text-gray-800'}`}>
{user.isAdmin ? 'Admin' : 'User'}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{activeTab === 'upload' && <DocumentUpload />}
</>
)}
</div>
</div>
</div>
);
}
export default AdminPanel;

View File

@ -0,0 +1,108 @@
import { useState } from 'react';
import { uploadDocument } from '../../services/api';
function DocumentUpload() {
const [file, setFile] = useState(null);
const [title, setTitle] = useState('');
const [status, setStatus] = useState('idle'); // idle, uploading, completed, failed
const [errorMessage, setErrorMessage] = useState('');
const handleFileChange = (event) => {
const selectedFile = event.target.files[0];
setFile(selectedFile);
};
const handleSubmit = async (event) => {
event.preventDefault();
if (!file || !title) {
setErrorMessage('Please provide both a title and a file');
return;
}
setStatus('uploading');
setErrorMessage('');
const formData = new FormData();
formData.append('file', file);
formData.append('title', title);
try {
await uploadDocument(formData);
setStatus('completed');
setTitle('');
setFile(null);
// Reset form
event.target.reset();
} catch (error) {
setStatus('failed');
setErrorMessage(error.response?.data?.message || 'Upload failed. Please try again.');
}
};
return (
<div className="max-w-md mx-auto mt-8 p-6 bg-white rounded-lg shadow-md">
<h2 className="text-2xl font-bold mb-6">Upload Document</h2>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700">
Document Title
</label>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
File
</label>
<input
type="file"
onChange={handleFileChange}
className="mt-1 block w-full"
required
/>
</div>
{errorMessage && (
<div className="text-red-500 text-sm mt-2">
{errorMessage}
</div>
)}
<button
type="submit"
disabled={status === 'uploading'}
className={`w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white
${status === 'uploading'
? 'bg-gray-400 cursor-not-allowed'
: 'bg-indigo-600 hover:bg-indigo-700'
}`}
>
{status === 'uploading' ? (
<>
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Uploading...
</>
) : 'Upload Document'}
</button>
{status === 'completed' && (
<div className="text-green-500 text-sm mt-2">
Document uploaded successfully!
</div>
)}
</form>
</div>
);
}
export default DocumentUpload;

View File

@ -16,12 +16,26 @@ api.interceptors.request.use((config) => {
});
export const createUser = (userData) => api.post('/admin/users', userData);
export const login = (username, password) => api.post('/auth/login', { username, password });
export const getAllUsers = () => api.get('/admin/users');
// export const getAllUsers = () => api.get('/admin/users');
export const shareDocument = (documentId, userIds) => api.post(`/admin/documents/${documentId}/share`, { userIds });
export const updateDocumentStatus = (documentId, status) => api.put(`/admin/documents/${documentId}/status`, { status });
export const uploadDocument = (formData) => api.post('/admin/documents', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
// export const uploadDocument = (formData) => api.post('/admin/documents', formData, {
// headers: { 'Content-Type': 'multipart/form-data' },
// });
export const uploadDocument = async (formData) => {
const response = await api.post('/admin/documents', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
return response.data;
};
export const getUserInfo = () => api.get('/auth/user-info');
// ... existing code ...
export const getAllDocuments = () => api.get('/admin/documents');
export const getAllUsers = () => api.get('/admin/users');
export default api;