till download files, everything works
This commit is contained in:
1 parent
ce893a4185
commit
ed1a580b09
6050 files changed
+768543
-177
No files matched your search
@@ -1,45 +1,153 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { getUserInfo } from '../../services/api';
|
||||
import * as jwtDecode from 'jwt-decode';
|
||||
import { motion } from 'framer-motion';
|
||||
import { format } from 'date-fns';
|
||||
import { FiFolder, FiDownload, FiChevronRight, FiChevronDown } from 'react-icons/fi';
|
||||
import { useAuth } from '../../hooks/useAuth';
|
||||
import { getSharedDocuments } from '../../services/api';
|
||||
|
||||
function Dashboard() {
|
||||
const [userInfo, setUserInfo] = useState(null);
|
||||
const [documents, setDocuments] = useState([]);
|
||||
const [expandedFolders, setExpandedFolders] = useState({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const { user } = useAuth();
|
||||
|
||||
useEffect(() => {
|
||||
fetchUserInfo();
|
||||
}, []);
|
||||
const fetchDocuments = async () => {
|
||||
try {
|
||||
const response = await getSharedDocuments(user.id);
|
||||
const groupedDocs = groupDocumentsByCompanyAndDate(response.data);
|
||||
setDocuments(groupedDocs);
|
||||
} catch (err) {
|
||||
setError('Failed to fetch documents');
|
||||
console.error('Error fetching documents:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchUserInfo = async () => {
|
||||
if (user) {
|
||||
fetchDocuments();
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
const groupDocumentsByCompanyAndDate = (docs) => {
|
||||
return docs.reduce((acc, doc) => {
|
||||
const folderName = `${doc.sharedWith?.name || 'Unknown'}-${format(new Date(doc.createdAt), 'yyyy-MM-dd')}`;
|
||||
if (!acc[folderName]) {
|
||||
acc[folderName] = [];
|
||||
}
|
||||
acc[folderName].push(doc);
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
|
||||
const handleDownload = async (s3Key, fileName) => {
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
const decodedToken = jwtDecode.jwtDecode(token);
|
||||
const response = await getUserInfo(decodedToken.sub);
|
||||
setUserInfo(response.data);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch user info:', error);
|
||||
window.open(`http://localhost:3000/documents/shared/download/${s3Key}`, '_blank');
|
||||
} catch (err) {
|
||||
console.error('Error downloading document:', err);
|
||||
alert('Failed to download document. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
if (!userInfo) {
|
||||
return <div>Loading...</div>;
|
||||
const toggleFolder = (folderName) => {
|
||||
setExpandedFolders(prev => ({
|
||||
...prev,
|
||||
[folderName]: !prev[folderName]
|
||||
}));
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex justify-center items-center min-h-screen">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="text-center text-red-600 p-4">
|
||||
{error}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="dashboard">
|
||||
<h1>Welcome, {userInfo.name}!</h1>
|
||||
<p>Email: {userInfo.email}</p>
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<h1 className="text-2xl font-bold mb-6">Your Laboratory Reports</h1>
|
||||
|
||||
<h2>Quick Links</h2>
|
||||
<ul>
|
||||
<li><Link to="/documents">My Documents</Link></li>
|
||||
<li><Link to="/upload">Upload Document</Link></li>
|
||||
{userInfo.isAdmin && <li><Link to="/admin">Admin Panel</Link></li>}
|
||||
</ul>
|
||||
{Object.entries(documents).length === 0 ? (
|
||||
<p className="text-gray-600">No documents have been shared with you yet.</p>
|
||||
) : (
|
||||
<div className="grid gap-4">
|
||||
{Object.entries(documents).map(([folderName, folderDocs]) => (
|
||||
<motion.div
|
||||
key={folderName}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
className="bg-white p-4 rounded-lg shadow-md"
|
||||
>
|
||||
<button
|
||||
onClick={() => toggleFolder(folderName)}
|
||||
className="w-full flex items-center text-left p-2 hover:bg-gray-50 rounded-md"
|
||||
>
|
||||
<FiFolder className="text-blue-600 mr-2" />
|
||||
{expandedFolders[folderName] ? (
|
||||
<FiChevronDown className="mr-2" />
|
||||
) : (
|
||||
<FiChevronRight className="mr-2" />
|
||||
)}
|
||||
<span className="font-medium">{folderName}</span>
|
||||
<span className="ml-2 text-sm text-gray-500">
|
||||
({folderDocs.length} {folderDocs.length === 1 ? 'report' : 'reports'})
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<h2>Recent Activity</h2>
|
||||
{/* You can add a list of recent activities here */}
|
||||
<p>No recent activity to display.</p>
|
||||
{expandedFolders[folderName] && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: 'auto' }}
|
||||
className="mt-2 space-y-2 pl-8"
|
||||
>
|
||||
{folderDocs.map((doc) => (
|
||||
<div
|
||||
key={doc.id}
|
||||
className="bg-white p-6 rounded-lg shadow-md hover:shadow-lg transition-shadow"
|
||||
>
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<h2 className="text-xl font-semibold">{doc.title}</h2>
|
||||
<span className={`px-2 py-1 text-xs rounded-full ${
|
||||
doc.status === 'completed'
|
||||
? 'bg-green-100 text-green-800'
|
||||
: 'bg-yellow-100 text-yellow-800'
|
||||
}`}>
|
||||
{doc.status}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-gray-600">
|
||||
<p>Created: {new Date(doc.createdAt).toLocaleDateString()}</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex justify-end">
|
||||
<button
|
||||
onClick={() => handleDownload(doc.s3Key, doc.title)}
|
||||
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 transition-colors"
|
||||
>
|
||||
<FiDownload className="inline-block mr-2" />
|
||||
Download
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
)}
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,42 +1,159 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { login } from '../../services/api';
|
||||
import { motion } from 'framer-motion';
|
||||
import axios from 'axios';
|
||||
|
||||
function Login() {
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const Login = () => {
|
||||
const navigate = useNavigate();
|
||||
const [formData, setFormData] = useState({
|
||||
username: '',
|
||||
password: ''
|
||||
});
|
||||
const [error, setError] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleInputChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[name]: value
|
||||
}));
|
||||
setError('');
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
const response = await login(username, password);
|
||||
localStorage.setItem('token', response.data.access_token);
|
||||
navigate('/dashboard');
|
||||
} catch (error) {
|
||||
console.error('Login failed:', error);
|
||||
const response = await axios.post('http://localhost:3000/auth/login', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (response.data.access_token) {
|
||||
// Store the token
|
||||
localStorage.setItem('token', response.data.access_token);
|
||||
|
||||
// Redirect based on user role (assuming you have this info)
|
||||
const isAdmin = response.data.isAdmin; // Adjust based on your API response
|
||||
navigate(isAdmin ? '/admin' : '/dashboard');
|
||||
} else {
|
||||
setError('Invalid response from server');
|
||||
}
|
||||
} catch (err) {
|
||||
setError(
|
||||
err.response?.data?.message ||
|
||||
'Неуспешна најава. Проверете ги вашите податоци.'
|
||||
);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
placeholder="Username"
|
||||
required
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Password"
|
||||
required
|
||||
/>
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
<div className="min-h-screen bg-gradient-to-b from-gray-50 to-white flex items-center justify-center px-4">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="max-w-md w-full space-y-8 bg-white p-8 rounded-xl shadow-lg"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="text-center">
|
||||
<h2 className="text-3xl font-bold text-gray-900">
|
||||
Најавете се
|
||||
</h2>
|
||||
<p className="mt-2 text-sm text-gray-600">
|
||||
Внесете ги вашите податоци за најава
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Form */}
|
||||
<form onSubmit={handleSubmit} className="mt-8 space-y-6">
|
||||
<div className="space-y-4">
|
||||
{/* Username Field */}
|
||||
<div>
|
||||
<label
|
||||
htmlFor="username"
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
Корисничко име
|
||||
</label>
|
||||
<input
|
||||
id="username"
|
||||
name="username"
|
||||
type="text"
|
||||
required
|
||||
value={formData.username}
|
||||
onChange={handleInputChange}
|
||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm
|
||||
focus:border-blue-500 focus:ring-blue-500 sm:text-sm
|
||||
px-3 py-2 border"
|
||||
aria-label="Username"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Password Field */}
|
||||
<div>
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
Лозинка
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
required
|
||||
value={formData.password}
|
||||
onChange={handleInputChange}
|
||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm
|
||||
focus:border-blue-500 focus:ring-blue-500 sm:text-sm
|
||||
px-3 py-2 border"
|
||||
aria-label="Password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error Message */}
|
||||
{error && (
|
||||
<div
|
||||
className="text-red-500 text-sm text-center bg-red-50 p-2 rounded"
|
||||
role="alert"
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Submit Button */}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="w-full flex justify-center py-3 px-4 border border-transparent
|
||||
rounded-full text-sm font-semibold text-white bg-blue-600
|
||||
hover:bg-blue-700 focus:outline-none focus:ring-2
|
||||
focus:ring-offset-2 focus:ring-blue-500 transition-colors
|
||||
duration-300 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isLoading ? (
|
||||
<span className="flex items-center">
|
||||
<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>
|
||||
Најава во тек...
|
||||
</span>
|
||||
) : (
|
||||
'Најави се'
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default Login;
|
||||
Reference in new issue
Block a user