- Profile Picture\

Donation Tracking\
Validation Rejection\
Subscription Data Export\
Admin Dashboard Logo\
Admin Navbar Reorganization
This commit is contained in:
Koncept Kit
2025-12-18 17:04:50 +07:00
parent 9ed778db1c
commit 8c0d9a2a18
10 changed files with 2159 additions and 141 deletions

View File

@@ -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>