- Profile Picture\
Donation Tracking\ Validation Rejection\ Subscription Data Export\ Admin Dashboard Logo\ Admin Navbar Reorganization
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import api from '../../utils/api';
|
||||
import { Card } from '../../components/ui/card';
|
||||
import { Button } from '../../components/ui/button';
|
||||
import { Badge } from '../../components/ui/badge';
|
||||
import { ArrowLeft, Mail, Phone, MapPin, Calendar, Lock, AlertTriangle } from 'lucide-react';
|
||||
import { Avatar, AvatarImage, AvatarFallback } from '../../components/ui/avatar';
|
||||
import { ArrowLeft, Mail, Phone, MapPin, Calendar, Lock, AlertTriangle, Camera, Upload, Trash2 } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import ConfirmationDialog from '../../components/ConfirmationDialog';
|
||||
|
||||
@@ -19,8 +20,13 @@ const AdminUserView = () => {
|
||||
const [subscriptionsLoading, setSubscriptionsLoading] = useState(true);
|
||||
const [confirmDialogOpen, setConfirmDialogOpen] = useState(false);
|
||||
const [pendingAction, setPendingAction] = useState(null);
|
||||
const [uploadingPhoto, setUploadingPhoto] = useState(false);
|
||||
const [maxFileSizeMB, setMaxFileSizeMB] = useState(50);
|
||||
const [maxFileSizeBytes, setMaxFileSizeBytes] = useState(52428800);
|
||||
const fileInputRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchConfig();
|
||||
fetchUserProfile();
|
||||
fetchSubscriptions();
|
||||
}, [userId]);
|
||||
@@ -48,6 +54,80 @@ const AdminUserView = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const fetchConfig = async () => {
|
||||
try {
|
||||
const response = await api.get('/config');
|
||||
setMaxFileSizeMB(response.data.max_file_size_mb);
|
||||
setMaxFileSizeBytes(response.data.max_file_size_bytes);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch config, using defaults:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePhotoUpload = async (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
// Validate file type
|
||||
if (!file.type.startsWith('image/')) {
|
||||
toast.error('Please select an image file');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate file size
|
||||
if (file.size > maxFileSizeBytes) {
|
||||
toast.error(`File size must be less than ${maxFileSizeMB}MB`);
|
||||
return;
|
||||
}
|
||||
|
||||
setUploadingPhoto(true);
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const response = await api.post(`/admin/users/${userId}/upload-photo`, formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
});
|
||||
|
||||
// Update user state with new photo URL
|
||||
setUser(prev => ({
|
||||
...prev,
|
||||
profile_photo_url: response.data.profile_photo_url
|
||||
}));
|
||||
|
||||
toast.success('Profile photo updated successfully');
|
||||
} catch (error) {
|
||||
toast.error(error.response?.data?.detail || 'Failed to upload photo');
|
||||
} finally {
|
||||
setUploadingPhoto(false);
|
||||
// Reset file input
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handlePhotoDelete = async () => {
|
||||
if (!user?.profile_photo_url) return;
|
||||
|
||||
setUploadingPhoto(true);
|
||||
try {
|
||||
await api.delete(`/admin/users/${userId}/delete-photo`);
|
||||
|
||||
// Update user state to remove photo URL
|
||||
setUser(prev => ({
|
||||
...prev,
|
||||
profile_photo_url: null
|
||||
}));
|
||||
|
||||
toast.success('Profile photo deleted successfully');
|
||||
} catch (error) {
|
||||
toast.error(error.response?.data?.detail || 'Failed to delete photo');
|
||||
} finally {
|
||||
setUploadingPhoto(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleResetPasswordRequest = () => {
|
||||
setPendingAction({ type: 'reset_password' });
|
||||
setConfirmDialogOpen(true);
|
||||
@@ -141,9 +221,12 @@ const AdminUserView = () => {
|
||||
<Card className="p-8 bg-white rounded-2xl border border-[#ddd8eb] mb-8">
|
||||
<div className="flex items-start gap-6">
|
||||
{/* Avatar */}
|
||||
<div className="h-24 w-24 rounded-full bg-[#DDD8EB] flex items-center justify-center text-[#422268] font-semibold text-3xl">
|
||||
{user.first_name?.[0]}{user.last_name?.[0]}
|
||||
</div>
|
||||
<Avatar className="h-24 w-24 border-4 border-[#ddd8eb]">
|
||||
<AvatarImage src={user.profile_photo_url} alt={`${user.first_name} ${user.last_name}`} />
|
||||
<AvatarFallback className="bg-[#DDD8EB] text-[#422268] font-semibold text-3xl">
|
||||
{user.first_name?.[0]}{user.last_name?.[0]}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
{/* User Info */}
|
||||
<div className="flex-1">
|
||||
@@ -207,6 +290,37 @@ const AdminUserView = () => {
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Profile Photo Management */}
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handlePhotoUpload}
|
||||
className="hidden"
|
||||
/>
|
||||
|
||||
<Button
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={uploadingPhoto}
|
||||
variant="outline"
|
||||
className="border-2 border-[#81B29A] text-[#81B29A] hover:bg-[#E8F5E9] rounded-full px-4 py-2 disabled:opacity-50"
|
||||
>
|
||||
<Upload className="h-4 w-4 mr-2" />
|
||||
{uploadingPhoto ? 'Uploading...' : 'Upload Photo'}
|
||||
</Button>
|
||||
|
||||
{user.profile_photo_url && (
|
||||
<Button
|
||||
onClick={handlePhotoDelete}
|
||||
disabled={uploadingPhoto}
|
||||
variant="outline"
|
||||
className="border-2 border-red-500 text-red-500 hover:bg-red-50 rounded-full px-4 py-2 disabled:opacity-50"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
Delete Photo
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2 text-sm text-[#664fa3] ml-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<span>User will receive a temporary password via email</span>
|
||||
|
||||
Reference in New Issue
Block a user