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

@ -47,3 +47,5 @@ model Notification {
user User @relation(fields: [userId], references: [id]) user User @relation(fields: [userId], references: [id])
createdAt DateTime @default(now()) 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 { PrismaService } from '../prisma/prisma.service';
import { S3Service } from '../s3/s3.service'; import { S3Service } from '../s3/s3.service';
import { CreateDocumentDto } from '../dto/create-document.dto'; import { CreateDocumentDto } from '../dto/create-document.dto';
@ -85,7 +85,6 @@ export class AdminService {
}); });
} }
async getAllDocuments() { async getAllDocuments() {
return this.prisma.document.findMany(); 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( async uploadDocument(
file: Express.Multer.File, file: Express.Multer.File,
title: string, title: string,
authorId: number, userId: number,
) { ) {
const key = `documents/${Date.now()}-${file.originalname}`; let s3Key;
const s3Key = await this.s3Service.uploadFile(file, key); try {
// First upload to S3
s3Key = await this.s3Service.uploadFile(file, 'documents');
return this.prisma.document.create({ // Then create document with completed status and s3Key
const document = await this.prisma.document.create({
data: { data: {
title, title,
authorId: userId,
s3Key, s3Key,
authorId, status: 'completed', // Set status to completed immediately after successful upload
status: 'completed',
}, },
}); });
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) { async getDocumentUrl(documentId: number) {

View File

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

View File

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

View File

@ -1,164 +1,238 @@
// 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 [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>
// <DocumentUpload />
// </div>
// );
// }
// export default AdminPanel;
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { getAllUsers, uploadDocument } from '../../services/api'; import { getAllUsers, getAllDocuments } from '../../services/api';
import DocumentUpload from '../documentUpload/DocumentUpload';
function AdminPanel() { function AdminPanel() {
const [activeTab, setActiveTab] = useState('documents');
const [users, setUsers] = useState([]); const [users, setUsers] = useState([]);
const [file, setFile] = useState(null); const [documents, setDocuments] = useState([]);
const [title, setTitle] = useState(''); const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
useEffect(() => { useEffect(() => {
fetchUsers(); fetchData();
}, []); }, [activeTab]);
const fetchUsers = async () => { const fetchData = async () => {
setLoading(true);
setError('');
try { try {
if (activeTab === 'users') {
const response = await getAllUsers(); const response = await getAllUsers();
setUsers(response.data); setUsers(response.data);
} catch (error) { } else if (activeTab === 'documents') {
console.error('Failed to fetch users:', error); const response = await getAllDocuments();
setDocuments(response.data);
}
} catch (err) {
setError('Failed to fetch data. Please try again.');
} finally {
setLoading(false);
} }
}; };
const handleFileUpload = async (e) => { const getStatusColor = (status) => {
e.preventDefault(); const colors = {
if (!file) return; pending: 'bg-yellow-100 text-yellow-800',
uploading: 'bg-blue-100 text-blue-800',
const formData = new FormData(); completed: 'bg-green-100 text-green-800',
formData.append('file', file); failed: 'bg-red-100 text-red-800'
formData.append('title', title); };
return colors[status] || 'bg-gray-100 text-gray-800';
try {
await uploadDocument(formData);
alert('Document uploaded successfully');
} catch (error) {
console.error('Failed to upload document:', error);
}
}; };
return ( return (
<div> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<h2>Users</h2> <div className="bg-white rounded-lg shadow">
<ul> {/* Admin Header */}
{users.map(user => ( <div className="px-6 py-4 border-b border-gray-200">
<li key={user.id}>{user.name} ({user.email})</li> <h1 className="text-2xl font-bold text-gray-900">Admin Dashboard</h1>
))} </div>
</ul>
<h2>Upload Document</h2> {/* Tab Navigation */}
<form onSubmit={handleFileUpload}> <div className="border-b border-gray-200">
<input <nav className="flex -mb-px">
type="text" <button
value={title} onClick={() => setActiveTab('documents')}
onChange={(e) => setTitle(e.target.value)} className={`px-6 py-3 border-b-2 font-medium text-sm ${
placeholder="Document Title" activeTab === 'documents'
required ? 'border-indigo-500 text-indigo-600'
/> : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
<input }`}
type="file" >
onChange={(e) => setFile(e.target.files[0])} Documents
required </button>
/> <button
<button type="submit">Upload</button> onClick={() => setActiveTab('users')}
</form> 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> </div>
); );
} }
export default AdminPanel; export default AdminPanel;
// import { useState } from 'react';
// import { uploadDocument } from '../../services/api';
// const AdminPanel = () => {
// 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]);
// }
// };
// const handleSubmit = async (event) => {
// event.preventDefault();
// if (!file || !title) {
// alert('Please select a file and enter a title');
// 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('');
// } catch (error) {
// console.error('Upload failed:', error);
// alert('Upload failed. Please try again.');
// } finally {
// setIsUploading(false);
// setProgress(0);
// }
// };
// 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>
// )}
// </form>
// </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 createUser = (userData) => api.post('/admin/users', userData);
export const login = (username, password) => api.post('/auth/login', { username, password }); 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 shareDocument = (documentId, userIds) => api.post(`/admin/documents/${documentId}/share`, { userIds });
export const updateDocumentStatus = (documentId, status) => api.put(`/admin/documents/${documentId}/status`, { status }); export const updateDocumentStatus = (documentId, status) => api.put(`/admin/documents/${documentId}/status`, { status });
export const uploadDocument = (formData) => api.post('/admin/documents', formData, { // export const uploadDocument = (formData) => api.post('/admin/documents', formData, {
headers: { 'Content-Type': 'multipart/form-data' }, // 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'); 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; export default api;