ai-helpdesk-api / Frontend /src /admin /pages /AdminProfile.jsx
ritesh19180's picture
Upload folder using huggingface_hub
d96d2a2 verified
Raw
History Blame Contribute Delete
25.8 kB
import React, { useState, useRef, useEffect } from 'react';
import {
User, Mail, Shield, Bell, Lock, Globe, Camera, ShieldCheck, Key,
Smartphone, History, Activity, CheckCircle2, AlertCircle, Copy,
LogOut, Eye, Save, X, Edit2, Download
} from 'lucide-react';
import useAuthStore from '../../store/authStore';
import useToastStore from '../../store/toastStore';
import { supabase } from "../../lib/supabaseClient";
import BugReportWidget from "../../components/shared/BugReportWidget";
const AdminProfile = () => {
const { user, profile: adminProfile } = useAuthStore();
const { showToast } = useToastStore();
const [isEditingProfile, setIsEditingProfile] = useState(false);
const [profileForm, setProfileForm] = useState({
name: adminProfile?.full_name || '',
email: adminProfile?.email || '',
profile_picture: adminProfile?.profile_picture || null
});
useEffect(() => {
if (adminProfile) {
setProfileForm({
name: adminProfile.full_name || '',
email: adminProfile.email || '',
profile_picture: adminProfile.profile_picture || null
});
}
}, [adminProfile]);
const [isAdmin2FAEnabled, setIsAdmin2FAEnabled] = useState(false);
const [showPasswordModal, setShowPasswordModal] = useState(false);
const [passwordForm, setPasswordForm] = useState({ newPassword: '', confirmPassword: '' });
const [passwordLoading, setPasswordLoading] = useState(false);
const fileInputRef = useRef(null);
const [activityLog, setActivityLog] = useState([]);
useEffect(() => {
const fetchActivity = async () => {
if (!adminProfile?.company) return;
try {
const { data, error } = await supabase
.from('tickets')
.select('id, subject, status, created_at')
.eq('company', adminProfile.company)
.order('created_at', { ascending: false })
.limit(5);
if (error) throw error;
const formatted = (data || []).map(t => ({
id: t.id,
action: `Ticket ${t.status?.toUpperCase() || 'UPDATED'}`,
target: `#TKT-${t.id.slice(0, 4)}`,
timestamp: new Date(t.created_at).toLocaleString(),
status: "Success"
}));
setActivityLog(formatted);
} catch (err) {
console.error("Failed to fetch activity log:", err);
}
};
fetchActivity();
}, [adminProfile?.company]);
const handleImageUpload = async (e) => {
const file = e.target.files?.[0];
if (!file) return;
try {
const userId = user?.id || adminProfile?.id;
const fileExt = file.name.split('.').pop();
const fileName = `${userId}/${Date.now()}.${fileExt}`;
const { error: uploadError } = await supabase.storage
.from('profile-pics').upload(fileName, file, { upsert: true, contentType: file.type || 'image/jpeg' });
if (uploadError) throw uploadError;
const { data } = supabase.storage.from('profile-pics').getPublicUrl(fileName);
setProfileForm(prev => ({ ...prev, profile_picture: data?.publicUrl }));
await supabase.from('profiles').update({ profile_picture: data?.publicUrl }).eq('id', userId);
await useAuthStore.getState().getProfile(user);
showToast("Profile picture updated.", "success");
} catch (err) {
showToast(`Upload failed: ${err.message}`, "error");
}
};
const handleSaveProfile = async () => {
try {
const { error } = await supabase.from('profiles').update({
full_name: profileForm.name, email: profileForm.email,
profile_picture: profileForm.profile_picture
}).eq('id', adminProfile.id);
if (error) throw error;
setIsEditingProfile(false);
await useAuthStore.getState().getProfile(useAuthStore.getState().user);
showToast("Profile updated successfully.", "success");
} catch (err) { showToast("Save failed: " + err.message, "error"); }
};
const handleDownloadArchive = () => {
const data = { admin_id: adminProfile.id, email: adminProfile.email, role: adminProfile.role, activity_logs: activityLog };
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a'); a.href = url;
a.download = `admin_archive_${adminProfile.id}.json`;
document.body.appendChild(a); a.click(); document.body.removeChild(a);
URL.revokeObjectURL(url);
};
const handleLogout = async () => {
await useAuthStore.getState().logout();
};
const handlePasswordChange = async () => {
if (!passwordForm.newPassword || passwordForm.newPassword.length < 6) {
showToast("Password must be at least 6 characters.", "error"); return;
}
if (passwordForm.newPassword !== passwordForm.confirmPassword) {
showToast("Passwords do not match", "error"); return;
}
setPasswordLoading(true);
try {
const { error } = await supabase.auth.updateUser({ password: passwordForm.newPassword });
if (error) throw error;
showToast("Password updated successfully", "success");
setShowPasswordModal(false);
setPasswordForm({ newPassword: '', confirmPassword: '' });
} catch (err) { showToast("Failed: " + err.message, "error"); }
finally { setPasswordLoading(false); }
};
const cs = { card: { background: '#fff', borderRadius: '24px', border: '1px solid #f0fdf4', boxShadow: '0 4px 24px rgba(0,0,0,0.06)', padding: '36px 40px' } };
return (
<div style={{ background: '#f8faf9', minHeight: '100vh', paddingBottom: '60px' }} className="max-w-6xl mx-auto py-6 space-y-10 -m-6 p-6 md:-m-10 md:p-10 animate-in fade-in duration-700">
{/* 1. Profile Hero */}
<div style={cs.card} className="flex flex-col md:flex-row items-center md:items-start gap-10 relative overflow-hidden">
<div className="relative group shrink-0">
<div style={{ width: 140, height: 140, borderRadius: '50%', border: '3px solid #bbf7d0', boxShadow: '0 0 0 6px rgba(34,160,69,0.08)', overflow: 'hidden', display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#f0fdf4', fontSize: '48px', fontWeight: 800, color: '#16a34a' }}>
{isEditingProfile && profileForm.profile_picture ? (
<img src={profileForm.profile_picture} alt="Profile" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
) : adminProfile?.profile_picture ? (
<img src={adminProfile.profile_picture} alt="Profile" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
) : (
isEditingProfile ? (profileForm.name?.charAt(0) || '?') : (adminProfile?.full_name?.charAt(0) || '?')
)}
</div>
{isEditingProfile && (
<>
<input type="file" ref={fileInputRef} onChange={handleImageUpload} className="hidden" accept="image/*" />
<button onClick={() => fileInputRef.current?.click()} style={{ position: 'absolute', bottom: -4, right: -4, background: '#fff', border: '1.5px solid #d1fae5', borderRadius: '12px', padding: '8px', cursor: 'pointer', color: '#15803d', boxShadow: '0 2px 8px rgba(0,0,0,0.08)', zIndex: 20 }}>
<Camera size={18} />
</button>
</>
)}
</div>
<div className="space-y-6 flex-1 text-center md:text-left">
<div className="flex flex-col md:flex-row md:justify-between md:items-start gap-4">
<div className="flex-1">
{!isEditingProfile ? (
<>
<h1 style={{ fontFamily: 'Syne, sans-serif', fontSize: '26px', fontWeight: 800, color: '#0f1f12', letterSpacing: '-0.02em', display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: '12px' }}>
{adminProfile?.full_name || 'Admin Agent'}
<span style={{ background: 'linear-gradient(135deg, #16a34a, #22c55e)', color: '#fff', borderRadius: '100px', fontSize: '11px', fontWeight: 700, letterSpacing: '0.08em', padding: '6px 16px', textTransform: 'uppercase' }}>
{adminProfile?.role === 'master_admin' ? 'Master Admin' : 'Admin'}
</span>
</h1>
<p style={{ color: '#9ca3af', fontSize: '11px', fontFamily: 'monospace', marginTop: '8px', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '6px' }} className="md:justify-start">
<ShieldCheck size={14} color="#16a34a" /> User ID: {adminProfile?.id}
</p>
</>
) : (
<div className="space-y-4 max-w-sm mx-auto md:mx-0">
<input type="text" value={profileForm.name} onChange={(e) => setProfileForm({ ...profileForm, name: e.target.value })} className="w-full bg-gray-50 border border-gray-200 rounded-xl px-4 py-3 text-lg font-bold focus:ring-2 focus:ring-emerald-500/20 focus:border-emerald-500 transition-all outline-none" placeholder="Admin Name" />
<input type="email" value={profileForm.email} onChange={(e) => setProfileForm({ ...profileForm, email: e.target.value })} className="w-full bg-gray-50 border border-gray-200 rounded-xl px-4 py-3 text-sm font-medium focus:ring-2 focus:ring-emerald-500/20 focus:border-emerald-500 transition-all outline-none" placeholder="Admin Email" />
</div>
)}
</div>
<div className="flex shrink-0 justify-center">
{!isEditingProfile ? (
<button onClick={() => { setProfileForm({ name: adminProfile?.full_name || '', email: adminProfile?.email || '', profile_picture: adminProfile?.profile_picture }); setIsEditingProfile(true); }}
style={{ background: '#fff', border: '1.5px solid #d1fae5', color: '#15803d', borderRadius: '10px', fontWeight: 600, fontSize: '13px', padding: '10px 20px', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '8px', transition: 'all 0.2s' }}>
<Edit2 size={15} /> Edit Profile
</button>
) : (
<div className="flex items-center gap-3">
<button onClick={() => setIsEditingProfile(false)} style={{ background: '#f3f4f6', color: '#6b7280', border: 'none', borderRadius: '10px', fontWeight: 600, fontSize: '13px', padding: '10px 20px', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '6px' }}>
<X size={15} /> Cancel
</button>
<button onClick={handleSaveProfile} style={{ background: 'linear-gradient(135deg, #16a34a, #22c55e)', color: '#fff', border: 'none', borderRadius: '10px', fontWeight: 600, fontSize: '13px', padding: '10px 20px', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '6px', boxShadow: '0 4px 12px rgba(22,163,74,0.3)' }}>
<Save size={15} /> Save Changes
</button>
</div>
)}
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
{[
{ label: 'Email Address', val: adminProfile?.email },
{ label: 'Company', val: adminProfile?.company || 'Universal Hub' },
{ label: 'Last Login', val: user?.last_sign_in_at ? new Date(user.last_sign_in_at).toLocaleString() : 'Just Now' }
].map((m, i) => (
<div key={i} style={{ padding: '16px', background: '#f8faf9', border: '1px solid #f0fdf4', borderRadius: '14px' }}>
<label style={{ fontSize: '10px', letterSpacing: '0.12em', color: '#9ca3af', fontWeight: 600, textTransform: 'uppercase', display: 'block', marginBottom: '4px' }}>{m.label}</label>
<span style={{ color: '#111827', fontWeight: 500, fontSize: '14px' }}>{m.val}</span>
</div>
))}
</div>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-12 gap-10">
{/* 2. Security */}
<div className="lg:col-span-5 space-y-10">
<div style={{ background: '#fff', borderRadius: '20px', border: '1px solid #f0fdf4', boxShadow: '0 2px 16px rgba(0,0,0,0.05)', overflow: 'hidden' }}>
<div style={{ background: '#0f1f12', padding: '16px 20px', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<h3 style={{ fontFamily: 'Syne, sans-serif', fontWeight: 700, color: '#fff', fontSize: '15px', display: 'flex', alignItems: 'center', gap: '8px', margin: 0 }}>
<Lock size={16} color="#22c55e" /> SECURITY SETTINGS
</h3>
<Shield size={16} color="#22c55e" style={{ opacity: 0.5 }} />
</div>
<div style={{ padding: '32px' }} className="space-y-8">
{/* Password */}
<div style={{ background: '#f8faf9', borderRadius: '14px', border: '1px solid #e5e7eb', padding: '20px' }} className="space-y-4">
<h4 style={{ fontSize: '11px', fontWeight: 700, color: '#111827', letterSpacing: '0.1em', textTransform: 'uppercase', display: 'flex', alignItems: 'center', gap: '8px' }}>
<Key size={14} color="#16a34a" /> Password
</h4>
<p style={{ fontSize: '12px', color: '#6b7280' }}>Update your administrative password to keep your account secure.</p>
<button onClick={() => setShowPasswordModal(true)} style={{ width: '100%', padding: '12px', background: '#fff', border: '1.5px solid #d1fae5', color: '#15803d', borderRadius: '10px', fontWeight: 600, fontSize: '13px', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '8px', transition: 'all 0.2s' }}>
<Edit2 size={14} /> Change Password
</button>
</div>
{/* 2FA */}
<div style={{ background: '#f8faf9', borderRadius: '14px', border: '1px solid #e5e7eb', padding: '20px', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '16px' }}>
<div className="space-y-1 flex-1">
<h4 style={{ fontSize: '11px', fontWeight: 700, color: '#111827', letterSpacing: '0.1em', textTransform: 'uppercase', display: 'flex', alignItems: 'center', gap: '8px' }}>
<Smartphone size={14} color="#16a34a" /> Two-Factor Authentication (2FA)
</h4>
<p style={{ fontSize: '11px', color: '#6b7280' }}>Enforce extra verification for secure admin actions.</p>
</div>
<button onClick={() => setIsAdmin2FAEnabled(!isAdmin2FAEnabled)} style={{ width: '52px', height: '28px', borderRadius: '100px', position: 'relative', transition: 'all 0.4s', background: isAdmin2FAEnabled ? '#22c55e' : '#d1d5db', border: 'none', cursor: 'pointer', flexShrink: 0 }}>
<div style={{ position: 'absolute', top: '2px', width: '24px', height: '24px', background: '#fff', borderRadius: '50%', transition: 'all 0.4s', boxShadow: '0 1px 3px rgba(0,0,0,0.15)', left: isAdmin2FAEnabled ? '26px' : '2px' }}></div>
</button>
</div>
{/* Bug Report */}
<div style={{ border: '1px solid #fee2e2', background: '#fff5f5', borderRadius: '14px', padding: '20px' }} className="space-y-4">
<h4 style={{ fontSize: '11px', fontWeight: 700, color: '#991b1b', letterSpacing: '0.1em', textTransform: 'uppercase', display: 'flex', alignItems: 'center', gap: '8px' }}>
<AlertCircle size={14} color="#dc2626" /> Bug Report
</h4>
<p style={{ fontSize: '11px', color: '#6b7280' }}>Submit a detailed system bug report with attachments.</p>
<BugReportWidget advanced={true} customTrigger={
<button style={{ width: '100%', padding: '12px', background: '#fff', border: '1.5px solid #fecaca', color: '#dc2626', borderRadius: '10px', fontWeight: 600, fontSize: '13px', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '8px' }}>
<AlertCircle size={14} /> Report Bug
</button>
} />
</div>
<button onClick={handleLogout} style={{ width: '100%', padding: '14px', background: '#fff5f5', border: '1.5px solid #fecaca', color: '#dc2626', borderRadius: '12px', fontWeight: 600, fontSize: '13px', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '10px', transition: 'all 0.2s', textTransform: 'uppercase', letterSpacing: '0.06em' }}>
<LogOut size={16} /> Logout
</button>
</div>
</div>
</div>
{/* 3. Activity Audit */}
<div className="lg:col-span-7">
<div style={{ background: '#fff', borderRadius: '20px', border: '1px solid #f0fdf4', boxShadow: '0 2px 16px rgba(0,0,0,0.05)', overflow: 'hidden', height: '100%', display: 'flex', flexDirection: 'column' }}>
<div style={{ padding: '20px 28px', borderBottom: '1px solid #f0fdf4', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<h3 style={{ fontFamily: 'Syne, sans-serif', fontSize: '15px', fontWeight: 700, color: '#0f1f12', display: 'flex', alignItems: 'center', gap: '8px', margin: 0 }}>
<History size={18} color="#16a34a" /> ACTIVITY LOG
</h3>
<button onClick={handleDownloadArchive} style={{ display: 'flex', alignItems: 'center', gap: '6px', padding: '8px 14px', background: '#fff', border: '1.5px solid #d1fae5', color: '#15803d', borderRadius: '10px', fontSize: '11px', fontWeight: 600, cursor: 'pointer', textTransform: 'uppercase', letterSpacing: '0.06em' }}>
<Download size={13} /> Download Log
</button>
</div>
<div className="flex-1 overflow-x-auto">
<table className="w-full border-collapse">
<thead>
<tr style={{ background: '#f8faf9' }}>
{['Action', 'Target', 'Time', 'Status'].map((h, i) => (
<th key={i} style={{ padding: '14px 24px', textAlign: i === 3 ? 'center' : 'left', fontSize: '10px', color: '#9ca3af', letterSpacing: '0.1em', fontWeight: 600, textTransform: 'uppercase' }}>{h}</th>
))}
</tr>
</thead>
<tbody>
{activityLog.map((log) => (
<tr key={log.id} className="hover:bg-[#f0fdf4] transition-colors">
<td style={{ padding: '16px 24px', fontSize: '13px', fontWeight: 600, color: '#111827' }}>{log.action}</td>
<td style={{ padding: '16px 24px' }}>
<span style={{ fontFamily: 'monospace', fontSize: '11px', fontWeight: 700, color: '#16a34a', background: '#f0fdf4', padding: '4px 8px', borderRadius: '6px' }}>{log.target}</span>
</td>
<td style={{ padding: '16px 24px', fontSize: '11px', color: '#6b7280' }}>{log.timestamp}</td>
<td style={{ padding: '16px 24px', textAlign: 'center' }}>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '4px', background: '#dcfce7', color: '#15803d', borderRadius: '100px', padding: '4px 10px', fontSize: '10px', fontWeight: 700 }}>
<CheckCircle2 size={12} /> {log.status}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
<div style={{ padding: '24px', textAlign: 'center', background: '#f8faf9' }}>
<p style={{ fontSize: '10px', color: '#9ca3af', letterSpacing: '0.14em', fontWeight: 600 }}>End of Activity Log</p>
</div>
</div>
</div>
</div>
{/* Password Modal */}
{showPasswordModal && (
<div className="fixed inset-0 bg-black/40 backdrop-blur-sm z-[100] flex items-center justify-center p-6 animate-in fade-in duration-300">
<div style={{ background: '#fff', borderRadius: '20px', boxShadow: '0 25px 50px rgba(0,0,0,0.15)', overflow: 'hidden', width: '100%', maxWidth: '400px' }} className="animate-in zoom-in-95 duration-300">
<div style={{ background: '#0f1f12', padding: '20px 24px', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<h3 style={{ fontFamily: 'Syne, sans-serif', fontWeight: 700, color: '#fff', fontSize: '16px', margin: 0 }}>Update Password</h3>
<button onClick={() => setShowPasswordModal(false)} style={{ color: '#9ca3af', background: 'none', border: 'none', cursor: 'pointer' }}><X size={20} /></button>
</div>
<div style={{ padding: '28px' }} className="space-y-5">
{[{ label: 'New Password', key: 'newPassword', ph: 'Enter new password' }, { label: 'Confirm Password', key: 'confirmPassword', ph: 'Confirm password' }].map((f, i) => (
<div key={i} className="space-y-1.5">
<label style={{ fontSize: '10px', fontWeight: 600, color: '#9ca3af', letterSpacing: '0.1em', textTransform: 'uppercase' }}>{f.label}</label>
<input type="password" placeholder={f.ph} className="w-full bg-gray-50 border border-gray-200 rounded-xl px-4 py-3 text-sm focus:ring-2 focus:ring-emerald-500/20 focus:border-emerald-500 transition-all outline-none"
value={passwordForm[f.key]} onChange={(e) => setPasswordForm({ ...passwordForm, [f.key]: e.target.value })} />
</div>
))}
<button onClick={handlePasswordChange} disabled={passwordLoading}
style={{ width: '100%', padding: '14px', background: 'linear-gradient(135deg, #16a34a, #22c55e)', color: '#fff', border: 'none', borderRadius: '12px', fontWeight: 600, fontSize: '13px', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '8px', boxShadow: '0 4px 12px rgba(22,163,74,0.3)' }}>
{passwordLoading ? 'Updating...' : <><Save size={15} /> Update Password</>}
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default AdminProfile;