SentinelScan-WSS / frontend /src /pages /Settings.jsx
grateful790's picture
Add Razorpay checkout with platform fee and INR/USD pricing
335a9ba
Raw History Blame Contribute Delete
75.6 kB
import { useState, useEffect } from 'react'; import { useAuth } from '../components/AuthContext'; import toast from 'react-hot-toast'; import Profile from './Profile'; export const AlertSettingsPage = () => { const { token, user, reloadUser } = useAuth(); const [activeTab, setActiveTab] = useState(() => { return localStorage.getItem('settingsActiveTab') || 'profile'; }); // profile, notifications, apiKeys, team, billing, scheduler useEffect(() => { localStorage.setItem('settingsActiveTab', activeTab); }, [activeTab]); // Notification states (connected to backend) const [emailNotifications, setEmailNotifications] = useState(true); const [webhookUrl, setWebhookUrl] = useState(''); const [severityThreshold, setSeverityThreshold] = useState('Medium'); const [reportLogoUrl, setReportLogoUrl] = useState(''); const [teamUsers, setTeamUsers] = useState([]); const [loadingTeam, setLoadingTeam] = useState(false); const [newUserFirstName, setNewUserFirstName] = useState(''); const [newUserLastName, setNewUserLastName] = useState(''); const [newUserEmail, setNewUserEmail] = useState(''); const [newUserPassword, setNewUserPassword] = useState(''); const [newUserRole, setNewUserRole] = useState('soc_analyst'); const [invitingUser, setInvitingUser] = useState(false); const [showAddMember, setShowAddMember] = useState(false); const [showNewUserPassword, setShowNewUserPassword] = useState(false); const [editingUser, setEditingUser] = useState(null); const [updatingUser, setUpdatingUser] = useState(false); const [userToDelete, setUserToDelete] = useState(null); const [deletingUser, setDeletingUser] = useState(false); const [passwordData, setPasswordData] = useState({ currentPassword: '', newPassword: '', confirmPassword: '' }); const [passwordStatus, setPasswordStatus] = useState({ loading: false, error: null, success: false }); const [showPassword, setShowPassword] = useState({ current: false, new: false, confirm: false }); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [message, setMessage] = useState(''); const [error, setError] = useState(''); const [billingHistory, setBillingHistory] = useState([]); const [loadingBilling, setLoadingBilling] = useState(false); const [scanQuotas, setScanQuotas] = useState([]); const [loadingQuotas, setLoadingQuotas] = useState(false); // Demo Bookings State const [demoBookings, setDemoBookings] = useState([]); const [loadingDemoBookings, setLoadingDemoBookings] = useState(false); const [rescheduleModal, setRescheduleModal] = useState({ isOpen: false, bookingId: null, email: '', meetingDate: '', meetingTime: '', status: 'rescheduled' }); const handleUpdateDemoBookingStatus = async (bookingId, status) => { try { const res = await fetch(`/api/demo/bookings/${bookingId}`, { method: 'PUT', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ status }) }); if (res.ok) { toast.success(`Booking status updated to ${status}`); fetchDemoBookings(); } else { toast.error("Failed to update status"); } } catch (err) { toast.error("Error updating status"); } }; const handleOpenReschedule = (booking) => { setRescheduleModal({ isOpen: true, bookingId: booking.id, email: booking.email, meetingDate: booking.meeting_date || '', meetingTime: booking.meeting_time || '', status: 'rescheduled' }); }; const handleSaveReschedule = async (e) => { e.preventDefault(); try { const res = await fetch(`/api/demo/bookings/${rescheduleModal.bookingId}`, { method: 'PUT', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ meeting_date: rescheduleModal.meetingDate, meeting_time: rescheduleModal.meetingTime, status: rescheduleModal.status }) }); if (res.ok) { toast.success("Demo booking rescheduled successfully!"); setRescheduleModal({ isOpen: false, bookingId: null, email: '', meetingDate: '', meetingTime: '', status: 'rescheduled' }); fetchDemoBookings(); } else { toast.error("Failed to reschedule demo booking"); } } catch (err) { toast.error("Error rescheduling booking"); } }; const handleDeleteDemoBooking = async (bookingId) => { if (!window.confirm("Are you sure you want to delete this booking lead?")) return; try { const res = await fetch(`/api/demo/bookings/${bookingId}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}` } }); if (res.ok) { toast.success("Booking lead deleted successfully"); fetchDemoBookings(); } else { toast.error("Failed to delete booking lead"); } } catch (err) { toast.error("Error deleting lead"); } }; // Scheduled Scans const [scheduledScans, setScheduledScans] = useState([]); const [loadingScans, setLoadingScans] = useState(false); const [newSchedule, setNewSchedule] = useState({ target_url: '', scan_type: 'Full', schedule_time: '20:00' }); // Notification History const [notificationHistory, setNotificationHistory] = useState([]); const [loadingHistory, setLoadingHistory] = useState(false); const [sortTeamCol, setSortTeamCol] = useState('Name'); const [sortTeamDir, setSortTeamDir] = useState('asc'); const [sortBillCol, setSortBillCol] = useState('Date'); const [sortBillDir, setSortBillDir] = useState('desc'); const handleTeamSort = (column) => { if (column === 'Actions') return; if (sortTeamCol === column) { setSortTeamDir(sortTeamDir === 'asc' ? 'desc' : 'asc'); } else { setSortTeamCol(column); setSortTeamDir('asc'); } }; const getSortedTeam = () => { return [...teamUsers].sort((a, b) => { let aVal, bVal; switch (sortTeamCol) { case 'Name': aVal = `${a.first_name || ''} ${a.last_name || ''}`.trim(); bVal = `${b.first_name || ''} ${b.last_name || ''}`.trim(); break; case 'Email': aVal = a.email || ''; bVal = b.email || ''; break; case 'Assigned Role': aVal = a.role || ''; bVal = b.role || ''; break; case 'Status': aVal = a.status || ''; bVal = b.status || ''; break; default: return 0; } if (aVal < bVal) return sortTeamDir === 'asc' ? -1 : 1; if (aVal > bVal) return sortTeamDir === 'asc' ? 1 : -1; return 0; }); }; const handleBillSort = (column) => { if (sortBillCol === column) { setSortBillDir(sortBillDir === 'asc' ? 'desc' : 'asc'); } else { setSortBillCol(column); setSortBillDir('desc'); } }; const getSortedBilling = () => { return [...billingHistory].sort((a, b) => { let aVal, bVal; switch (sortBillCol) { case 'Date': aVal = new Date(a.created_at).getTime(); bVal = new Date(b.created_at).getTime(); break; case 'Plan': aVal = a.tier_id || ''; bVal = b.tier_id || ''; break; case 'Amount': aVal = a.amount || 0; bVal = b.amount || 0; break; case 'Status': aVal = a.status || ''; bVal = b.status || ''; break; default: return 0; } if (aVal < bVal) return sortBillDir === 'asc' ? -1 : 1; if (aVal > bVal) return sortBillDir === 'asc' ? 1 : -1; return 0; }); }; useEffect(() => { fetchSettings(); }, [token]); const fetchSettings = async () => { try { const res = await fetch('/api/vulnerabilities/settings', { headers: { 'Authorization': `Bearer ${token}` } }); if (res.ok) { const data = await res.json(); setEmailNotifications(data.settings.email_notifications); setWebhookUrl(data.settings.webhook_url || ''); setSeverityThreshold(data.settings.severity_threshold); } } catch (err) { console.error("Error loading alert settings", err); } finally { setLoading(false); } }; const fetchNotificationHistory = async () => { setLoadingHistory(true); try { const res = await fetch('/api/auth/notifications', { headers: { 'Authorization': `Bearer ${token}` } }); if (res.ok) { const data = await res.json(); setNotificationHistory(data.notifications || []); } } catch (err) { console.error("Error loading notification history", err); } finally { setLoadingHistory(false); } }; const fetchTeamUsers = async () => { setLoadingTeam(true); try { const endpoint = user?.org_id ? `/api/auth/organizations/${user.org_id}/users` : '/api/auth/users'; const res = await fetch(endpoint, { headers: { 'Authorization': `Bearer ${token}` } }); if (res.ok) { const data = await res.json(); const filteredUsers = (data.users || []).filter(u => u.role !== 'super_admin'); setTeamUsers(filteredUsers); } } catch (err) { console.error('Failed to fetch org users', err); } finally { setLoadingTeam(false); } }; useEffect(() => { if (activeTab === 'team' && (user?.role === 'org_admin' || user?.role === 'super_admin')) { fetchTeamUsers(); } }, [activeTab, user]); const handleInviteUser = async (e) => { e.preventDefault(); if (!newUserEmail) return; setInvitingUser(true); try { const res = await fetch('/api/auth/users/invite', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ email: newUserEmail, role: newUserRole, first_name: newUserFirstName, last_name: newUserLastName, password: newUserPassword }) }); const data = await res.json(); if (res.ok) { toast.success(`User added successfully!`); setNewUserFirstName(''); setNewUserLastName(''); setNewUserEmail(''); setNewUserPassword(''); setShowAddMember(false); setShowNewUserPassword(false); fetchTeamUsers(); } else { toast.error(data.message || 'Failed to invite user'); } } catch (err) { toast.error("Error inviting user"); } finally { setInvitingUser(false); } }; const handleUpdateUser = async (e) => { e.preventDefault(); if (!editingUser) return; setUpdatingUser(true); try { const res = await fetch(`/api/auth/users/${editingUser.id}`, { method: 'PUT', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ first_name: editingUser.first_name, last_name: editingUser.last_name, email: editingUser.email, password: editingUser.new_password, role: editingUser.role }) }); if (res.ok) { toast.success(`User updated successfully!`); setEditingUser(null); fetchTeamUsers(); } else { const data = await res.json(); toast.error(data.message || 'Failed to update user'); } } catch (err) { toast.error("Error updating user"); } finally { setUpdatingUser(false); } }; const executeDeleteUser = async () => { if (!userToDelete) return; setDeletingUser(true); try { const res = await fetch(`/api/auth/users/${userToDelete.id}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}` } }); if (res.ok) { toast.success("User removed successfully!"); setUserToDelete(null); fetchTeamUsers(); } else { const data = await res.json(); toast.error(data.message || "Failed to remove user"); } } catch (err) { toast.error("Error removing user"); } finally { setDeletingUser(false); } }; const handlePasswordChange = async (e) => { e.preventDefault(); setPasswordStatus({ loading: true, error: null, success: false }); if (passwordData.newPassword !== passwordData.confirmPassword) { setPasswordStatus({ loading: false, error: "New passwords do not match", success: false }); return; } if (passwordData.newPassword.length < 6) { setPasswordStatus({ loading: false, error: "New password must be at least 6 characters", success: false }); return; } try { const res = await fetch('/api/auth/password', { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ currentPassword: passwordData.currentPassword, newPassword: passwordData.newPassword }) }); let data = {}; try { data = await res.json(); } catch (e) { } if (res.ok) { setPasswordStatus({ loading: false, error: null, success: true }); setPasswordData({ currentPassword: '', newPassword: '', confirmPassword: '' }); toast.success("Password updated successfully!"); setTimeout(() => setPasswordStatus(prev => ({ ...prev, success: false })), 3000); } else { const errorMsg = data.message || "Failed to update password"; setPasswordStatus({ loading: false, error: errorMsg, success: false }); toast.error(errorMsg); } } catch (err) { setPasswordStatus({ loading: false, error: "Network error occurred", success: false }); toast.error("Network error occurred"); } }; // Auto-refresh user data (e.g., subscription upgrades) when Billing tab is active useEffect(() => { let intervalId; if (activeTab === 'billing' && reloadUser) { intervalId = setInterval(() => { reloadUser(); fetchQuotas(true); fetchBillingHistory(true); }, 15000); // Poll every 15 seconds silently } return () => { if (intervalId) clearInterval(intervalId); }; }, [activeTab, reloadUser]); useEffect(() => { if (activeTab === 'billing') { fetchBillingHistory(); fetchQuotas(); } if (activeTab === 'notifications') { fetchNotificationHistory(); } }, [activeTab, user?.org_id, user?.id]); const fetchQuotas = async (isSilent = false) => { if (!isSilent && scanQuotas.length === 0) setLoadingQuotas(true); try { // If user has organization_id, fetch from it. Otherwise we might fetch from a general endpoint if it existed, or we just try to fetch the first organization. // Usually users belong to one organization. Let's try to get their organization ID first, or fetch from /api/auth/organizations let orgId = user?.org_id; if (!orgId) { const orgRes = await fetch('/api/auth/organizations', { headers: { 'Authorization': `Bearer ${token}` } }); if (orgRes.ok) { const orgsData = await orgRes.json(); if (orgsData.organizations && orgsData.organizations.length > 0) { orgId = orgsData.organizations[0].id; } } } if (orgId) { const res = await fetch(`/api/auth/organizations/${orgId}/quotas`, { headers: { 'Authorization': `Bearer ${token}` } }); if (res.ok) { const data = await res.json(); setScanQuotas(data.quotas || []); } } } catch (err) { console.error("Error loading quotas", err); } finally { setLoadingQuotas(false); } }; const fetchBillingHistory = async (isSilent = false) => { if (!isSilent && billingHistory.length === 0) setLoadingBilling(true); try { const res = await fetch('/api/billing/history', { headers: { 'Authorization': `Bearer ${token}` } }); if (res.ok) { const data = await res.json(); setBillingHistory(data.history || []); } } catch (err) { console.error("Error loading billing history", err); } finally { setLoadingBilling(false); } }; useEffect(() => { if (activeTab === 'scheduler') { fetchScheduledScans(); } }, [activeTab, token]); const fetchScheduledScans = async () => { setLoadingScans(true); try { const res = await fetch('/api/scans/schedule', { headers: { 'Authorization': `Bearer ${token}` } }); if (res.ok) { const data = await res.json(); setScheduledScans(data.schedules || []); } } catch (err) { console.error("Error loading scheduled scans", err); } finally { setLoadingScans(false); } }; const fetchDemoBookings = async () => { setLoadingDemoBookings(true); try { const res = await fetch('/api/demo/bookings', { headers: { 'Authorization': `Bearer ${token}` } }); if (res.ok) { const data = await res.json(); setDemoBookings(data || []); } } catch (err) { console.error("Error loading demo bookings", err); } finally { setLoadingDemoBookings(false); } }; useEffect(() => { if (activeTab === 'demoBookings') { fetchDemoBookings(); } }, [activeTab, token]); const handleCreateSchedule = async (e) => { e.preventDefault(); setMessage(''); setError(''); if (user?.subscription_tier === 'Free') { setError('Scheduled scans require a premium subscription.'); return; } try { const res = await fetch('/api/scans/schedule', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ ...newSchedule, frequency: 'daily' }) }); const data = await res.json(); if (res.ok) { setMessage('Scan scheduled successfully!'); setNewSchedule({ target_url: '', scan_type: 'Full', schedule_time: '20:00' }); fetchScheduledScans(); } else { setError(data.message || 'Failed to schedule scan.'); } } catch (err) { setError('Connection error.'); } }; const handleDeleteSchedule = async (id) => { try { const res = await fetch(`/api/scans/schedule/${id}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}` } }); if (res.ok) { setScheduledScans(prev => prev.filter(s => s.id !== id)); } } catch (err) { console.error('Failed to delete schedule', err); } }; const handleSave = async (e) => { e.preventDefault(); setSaving(true); setMessage(''); setError(''); try { const res = await fetch('/api/vulnerabilities/settings', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ email_notifications: emailNotifications, webhook_url: webhookUrl, severity_threshold: severityThreshold }) }); const data = await res.json(); if (res.ok) { setMessage('Alert preferences successfully updated!'); } else { setError(data.message || 'Failed to update alert configurations.'); } } catch (err) { setError('Could not establish connection to the security server API.'); console.error(err); } finally { setSaving(false); } }; const isSchedulerAllowed = ['super_admin', 'support_engineer', 'admin', 'org_admin', 'soc_analyst'].includes(user?.role); const isDemoBookingsAllowed = ['super_admin', 'support_engineer', 'admin', 'org_admin', 'soc_analyst'].includes(user?.role); const isNotificationsAllowed = ['super_admin', 'support_engineer', 'admin', 'org_admin'].includes(user?.role); const isApiKeysAllowed = ['super_admin', 'support_engineer', 'admin', 'org_admin'].includes(user?.role); const isTeamAllowed = ['super_admin', 'support_engineer', 'admin', 'org_admin'].includes(user?.role); const isBillingAllowed = ['super_admin', 'support_engineer', 'admin', 'org_admin'].includes(user?.role); const tabItems = [ { id: 'profile', label: 'My Profile', icon: 'person' } ]; if (isSchedulerAllowed) tabItems.push({ id: 'scheduler', label: 'Scheduler', icon: 'schedule' }); if (isDemoBookingsAllowed) tabItems.push({ id: 'demoBookings', label: 'Demo Bookings', icon: 'event_available' }); if (isNotificationsAllowed) tabItems.push({ id: 'notifications', label: 'Notifications', icon: 'notifications' }); if (isApiKeysAllowed) tabItems.push({ id: 'apiKeys', label: 'API Keys', icon: 'key' }); if (isTeamAllowed) tabItems.push({ id: 'team', label: 'Team', icon: 'group' }); if (isBillingAllowed) tabItems.push({ id: 'billing', label: 'Billing', icon: 'credit_card' }); useEffect(() => { const isAllowed = tabItems.some(t => t.id === activeTab); if (!isAllowed) { setActiveTab('profile'); } }, [user?.role, activeTab]); if (loading) { return ( <div className="flex items-center justify-center py-2xl font-label-md text-label-md text-on-surface-variant text-left"> <span className="material-symbols-outlined animate-spin mr-sm">sync</span> Loading Settings... </div> ); } // Pre-fill user profile info or fallback to Mercer placeholder const profileName = user ? user.email.split('@')[0] : 'Alex'; const profileLastName = user ? 'User' : 'Mercer'; const profileEmail = user ? user.email : 'alex.mercer@larxiuswss.io'; const profileRole = user?.role ? `Role: ${user.role}` : 'Lead Security Engineer'; return ( <div className="flex flex-col gap-gutter text-left w-full"> {/* Settings Header & Tabs */} <div className="border-b border-outline-variant bg-surface-container-lowest p-lg rounded-xl shadow-sm mb-sm"> <h1 className="font-display-lg text-display-lg text-on-surface mb-sm font-bold tracking-tight">Settings</h1> <p className="font-body-lg text-body-lg text-on-surface-variant mb-xl"> Manage your account preferences, security protocols, and team access. </p> {/* Scrollable Tabs row */} <div className="flex items-center gap-md md:gap-lg overflow-x-auto scrollbar-none"> {tabItems.map((tab) => { const isActive = activeTab === tab.id; return ( <button key={tab.id} onClick={() => { setActiveTab(tab.id); setMessage(''); setError(''); }} className={`font-label-md text-label-md pb-sm px-sm transition-all flex items-center gap-xs cursor-pointer border-0 bg-transparent whitespace-nowrap ${isActive ? 'text-primary border-b-2 border-primary font-bold' : 'text-on-surface-variant hover:text-primary' }`} > <span className="material-symbols-outlined text-[18px]">{tab.icon}</span> {tab.label} </button> ); })} </div> </div> {/* Tab Switch Contents */} {activeTab === 'scheduler' && isSchedulerAllowed && ( <div className="max-w-3xl mx-auto w-full"> <div className="bg-surface-container-lowest border border-outline-variant rounded-lg p-lg shadow-sm mb-lg"> <h2 className="font-headline-md text-headline-md font-bold mb-md flex items-center gap-sm border-b pb-sm"> <span className="material-symbols-outlined text-primary text-[22px]">schedule</span> Schedule Automated Scans </h2> <p className="font-body-sm text-body-sm text-on-surface-variant mb-lg"> Set up daily automated scans for your targets. The scanner will run automatically at your specified time. </p> {message && ( <div className="flex gap-sm bg-green-500/10 border border-green-500/30 rounded-lg p-md text-green-600 font-body-sm text-body-sm items-center mb-md"> <span className="material-symbols-outlined shrink-0 text-green-500">check_circle</span> <div>{message}</div> </div> )} {error && ( <div className="flex gap-sm bg-error-container/20 border border-error/30 rounded-lg p-md text-error font-body-sm text-body-sm items-center mb-md"> <span className="material-symbols-outlined shrink-0">error</span> <div>{error}</div> </div> )} <form onSubmit={handleCreateSchedule} className="flex flex-col gap-md"> <div className="flex gap-md w-full max-sm:flex-col"> <div className="flex-1 flex flex-col gap-xs"> <label className="font-label-sm text-label-sm font-bold">Target URL</label> <input type="url" required className="w-full bg-surface-container-low border border-outline-variant rounded-lg px-md py-sm focus:border-primary focus:ring-1 focus:ring-primary/50" placeholder="https://example.com" value={newSchedule.target_url} onChange={e => setNewSchedule({ ...newSchedule, target_url: e.target.value })} /> </div> <div className="w-1/4 max-sm:w-full flex flex-col gap-xs"> <label className="font-label-sm text-label-sm font-bold">Time (Daily)</label> <input type="time" required className="w-full bg-surface-container-low border border-outline-variant rounded-lg px-md py-sm focus:border-primary focus:ring-1 focus:ring-primary/50" value={newSchedule.schedule_time} onChange={e => setNewSchedule({ ...newSchedule, schedule_time: e.target.value })} /> </div> <div className="w-1/4 max-sm:w-full flex flex-col gap-xs"> <label className="font-label-sm text-label-sm font-bold">Scan Type</label> <select className="w-full bg-surface-container-low border border-outline-variant rounded-lg px-md py-sm focus:border-primary focus:ring-1 focus:ring-primary/50 cursor-pointer" value={newSchedule.scan_type} onChange={e => setNewSchedule({ ...newSchedule, scan_type: e.target.value })} > <option value="Quick">Quick</option> <option value="Full">Full</option> <option value="Deep">Deep</option> </select> </div> </div> <div className="flex justify-end mt-sm"> <button type="submit" className="bg-primary text-on-primary font-label-md py-sm px-lg rounded-lg hover:opacity-90 transition-opacity font-bold"> Add Schedule </button> </div> </form> </div> <div className="bg-surface-container-lowest border border-outline-variant rounded-lg p-lg shadow-sm"> <h3 className="font-headline-sm font-bold mb-md">Active Schedules</h3> {loadingScans ? ( <p className="text-on-surface-variant font-body-sm py-md">Loading schedules...</p> ) : scheduledScans.length === 0 ? ( <p className="text-on-surface-variant font-body-sm py-md">No automated scans scheduled yet.</p> ) : ( <div className="flex flex-col gap-sm"> {scheduledScans.map(scan => ( <div key={scan.id} className="flex items-center justify-between bg-surface-container-low p-md rounded-lg border border-outline-variant/50"> <div> <div className="font-bold font-body-md text-on-surface">{scan.target_url}</div> <div className="text-on-surface-variant font-body-sm flex items-center gap-md mt-xs whitespace-nowrap overflow-x-auto scrollbar-none w-full max-w-[calc(100vw-4rem)]"> <span className="flex items-center gap-xs"><span className="material-symbols-outlined text-[14px]">schedule</span> {scan.schedule_time} (Daily)</span> <span className="flex items-center gap-xs"><span className="material-symbols-outlined text-[14px]">troubleshoot</span> {scan.scan_type} Scan</span> {scan.last_run_at && <span className="flex items-center gap-xs text-primary"><span className="material-symbols-outlined text-[14px]">history</span> Last run: {new Date(scan.last_run_at).toLocaleDateString()}</span>} </div> </div> <button onClick={() => handleDeleteSchedule(scan.id)} className="text-error hover:bg-error/10 p-sm rounded-full transition-colors" title="Delete Schedule"> <span className="material-symbols-outlined">delete</span> </button> </div> ))} </div> )} </div> </div> )} {activeTab === 'demoBookings' && isDemoBookingsAllowed && ( <div className="max-w-3xl mx-auto w-full"> <div className="bg-surface-container-lowest border border-outline-variant rounded-lg p-lg shadow-sm mb-lg"> <h2 className="font-headline-md text-headline-md font-bold mb-md flex items-center gap-sm border-b pb-sm"> <span className="material-symbols-outlined text-primary text-[22px]">event_available</span> Discovery Call Bookings </h2> <div className="space-y-sm"> {loadingDemoBookings ? ( <div className="flex items-center justify-center p-xl"> <div className="w-8 h-8 border-4 border-primary/20 border-t-primary rounded-full animate-spin"></div> </div> ) : demoBookings.length > 0 ? ( demoBookings.map((b) => { const statusStyle = b.status === 'completed' ? 'bg-emerald-100 text-emerald-700 border-emerald-300' : b.status === 'cancelled' ? 'bg-rose-100 text-rose-700 border-rose-300' : b.status === 'rescheduled' ? 'bg-indigo-100 text-indigo-700 border-indigo-300' : 'bg-amber-100 text-amber-700 border-amber-300'; const statusLabel = b.status === 'completed' ? 'Completed' : b.status === 'cancelled' ? 'Cancelled / Not Conducted' : b.status === 'rescheduled' ? 'Rescheduled' : 'Pending'; return ( <div key={b.id} className={`bg-white border p-md rounded-lg shadow-sm flex flex-col sm:flex-row justify-between items-start sm:items-center gap-md ${b.status === 'completed' ? 'border-emerald-500/50' : 'border-outline-variant/60'}`}> <div> <div className="font-headline-sm font-bold text-on-surface mb-xs flex items-center gap-2 flex-wrap"> <span>{b.email}</span> <span className={`text-[10px] font-bold px-2 py-0.5 rounded-full border ${statusStyle}`}> {statusLabel} </span> </div> <div className="font-body-sm text-on-surface-variant flex flex-wrap gap-md mt-xs"> <span className="flex items-center gap-xs"><span className="material-symbols-outlined text-[14px]">calendar_today</span> {b.meeting_date}</span> <span className="flex items-center gap-xs"><span className="material-symbols-outlined text-[14px]">schedule</span> {b.meeting_time}</span> <span className="flex items-center gap-xs"><span className="material-symbols-outlined text-[14px]">business</span> {b.company_size}</span> </div> </div> <div className="flex flex-col sm:items-end gap-2 shrink-0 w-full sm:w-auto"> <div className="text-xs text-on-surface-variant bg-surface-container-low px-2 py-1 rounded w-max"> Booked: {new Date(b.created_at).toLocaleDateString()} </div> <div className="flex items-center gap-1.5 flex-wrap"> {b.status !== 'completed' && ( <button onClick={() => handleUpdateDemoBookingStatus(b.id, 'completed')} className="bg-emerald-600 text-white border-none py-1 px-2.5 rounded text-[11px] font-bold cursor-pointer hover:bg-emerald-700 transition-all flex items-center gap-1 shadow-xs" title="Mark Completed" > <span className="material-symbols-outlined text-[13px]">task_alt</span> Complete </button> )} {b.status !== 'cancelled' && ( <button onClick={() => handleUpdateDemoBookingStatus(b.id, 'cancelled')} className="bg-rose-50 text-rose-600 border border-rose-200 py-1 px-2.5 rounded text-[11px] font-bold cursor-pointer hover:bg-rose-600 hover:text-white transition-all flex items-center gap-1" title="Mark Cancelled / Not Conducted" > <span className="material-symbols-outlined text-[13px]">cancel</span> Cancelled </button> )} <button onClick={() => handleOpenReschedule(b)} className="bg-indigo-50 text-indigo-600 border border-indigo-200 py-1 px-2.5 rounded text-[11px] font-bold cursor-pointer hover:bg-indigo-600 hover:text-white transition-all flex items-center gap-1" title="Reschedule Date & Time" > <span className="material-symbols-outlined text-[13px]">edit_calendar</span> Reschedule </button> <button onClick={() => handleDeleteDemoBooking(b.id)} className="text-on-surface-variant hover:text-rose-600 border-0 bg-transparent p-1 cursor-pointer transition-colors" title="Delete Lead" > <span className="material-symbols-outlined text-[16px]">delete</span> </button> </div> </div> </div> ); }) ) : ( <div className="text-center py-xl bg-white border border-outline-variant/60 rounded-lg shadow-sm"> <span className="material-symbols-outlined text-outline text-[48px] mb-sm block">event_busy</span> <p className="font-body-md text-on-surface-variant">No demo bookings found.</p> </div> )} </div> </div> </div> )} {/* 2. Notifications Tab (History) */} {activeTab === 'notifications' && isNotificationsAllowed && ( <div className="max-w-3xl mx-auto w-full"> {/* Notification History Panel */} <div className="bg-surface-container-lowest border border-outline-variant rounded-lg p-lg shadow-sm flex flex-col max-h-[700px]"> <h2 className="font-headline-md text-headline-md font-bold mb-md flex items-center gap-sm border-b pb-sm shrink-0"> <span className="material-symbols-outlined text-primary text-[22px]">history</span> Notification History </h2> <div className="overflow-y-auto pr-2 space-y-sm flex-1 custom-scrollbar"> {loadingHistory ? ( <div className="flex justify-center p-xl"> <div className="w-6 h-6 border-2 border-primary/20 border-t-primary rounded-full animate-spin"></div> </div> ) : notificationHistory.length > 0 ? ( notificationHistory.map((notif, idx) => ( <div key={idx} className="bg-white border border-outline-variant/60 p-md rounded-lg shadow-sm flex gap-md items-start"> <div className={`p-sm rounded-full shrink-0 ${notif.bg} ${notif.color}`}> <span className="material-symbols-outlined text-[20px] block">{notif.icon}</span> </div> <div> <div className="font-headline-sm font-bold text-on-surface mb-xs">{notif.title}</div> <div className="font-body-sm text-on-surface-variant mb-xs">{notif.message}</div> <div className="text-[11px] text-on-surface-variant/70">{new Date(notif.timestamp).toLocaleString()}</div> </div> </div> )) ) : ( <div className="text-center py-xl bg-surface-container-low rounded-lg border border-outline-variant/30"> <span className="material-symbols-outlined text-outline text-[32px] mb-sm block">notifications_paused</span> <p className="font-body-sm text-on-surface-variant">No recent notifications.</p> </div> )} </div> </div> </div> )} {/* 3. Billing Tab */} {activeTab === 'billing' && isBillingAllowed && ( <div className="max-w-3xl mx-auto w-full flex flex-col gap-gutter"> {/* Subscription & Billing */} <div className="bg-surface-container-lowest border border-outline-variant rounded-lg p-lg shadow-sm"> <h2 className="font-headline-md text-headline-md font-bold mb-md flex items-center gap-sm border-b pb-sm"> <span className="material-symbols-outlined text-primary text-[22px]">credit_card</span> Subscription & Billing </h2> {/* Current Plan - Compact Horizontal Layout */} <div className="flex flex-col sm:flex-row items-center justify-between bg-surface-container border border-outline-variant rounded-md p-md mb-lg"> <div className="flex items-center gap-md"> <span className="material-symbols-outlined text-primary text-3xl">workspace_premium</span> <div> <p className="font-label-sm text-on-surface-variant uppercase tracking-wider mb-0">Current Plan</p> <div className="flex items-center gap-sm"> <h3 className="font-headline-sm font-bold text-on-surface capitalize m-0"> {user?.role === 'super_admin' ? 'Enterprise' : user?.subscription_tier || 'Free'} </h3> <span className={`px-sm py-xs rounded text-[10px] font-bold uppercase tracking-wider ${user?.role === 'super_admin' || user?.subscription_status === 'active' ? 'bg-emerald-500/10 text-emerald-500' : 'bg-error-container text-error' }`}> {user?.role === 'super_admin' ? 'Active' : user?.subscription_status || 'Inactive'} </span> </div> </div> </div> {user?.role !== 'super_admin' && ( <a href="/pricing" className="mt-sm sm:mt-0 bg-primary text-on-primary px-lg py-sm rounded-md font-label-md font-bold hover:bg-primary/90 transition-colors flex items-center gap-xs" > Upgrade <span className="material-symbols-outlined text-[18px]">arrow_forward</span> </a> )} </div> {/* Scan Quotas - Compact Grid */} <div> <h3 className="font-title-md font-bold text-on-surface mb-sm flex items-center gap-xs"> <span className="material-symbols-outlined text-on-surface-variant text-[18px]">pie_chart</span> Scan Quotas </h3> {loadingQuotas ? ( <div className="flex items-center justify-center py-xl text-on-surface-variant"> <span className="material-symbols-outlined animate-spin mr-sm">sync</span> Loading... </div> ) : scanQuotas.length > 0 ? ( <div className="grid grid-cols-1 sm:grid-cols-3 gap-md"> {scanQuotas.filter(q => ['Quick', 'Advanced', 'Deep'].includes(q.scan_type)).map((q, idx) => { const icon = q.scan_type.toLowerCase().includes('quick') ? 'bolt' : q.scan_type.toLowerCase().includes('advanced') ? 'security' : q.scan_type.toLowerCase().includes('deep') ? 'radar' : 'pie_chart'; return ( <div key={idx} className="bg-surface border border-outline-variant rounded-md p-sm flex items-center gap-sm shadow-sm"> <div className="bg-primary/10 text-primary p-xs rounded"> <span className="material-symbols-outlined text-xl">{icon}</span> </div> <div className="flex flex-col"> <span className="font-label-sm text-on-surface-variant uppercase">{q.scan_type}</span> <span className="font-title-md font-bold text-on-surface leading-tight"> {q.allocated_count === -1 ? 'Unlimited' : Math.max(0, q.allocated_count - (q.used_count || 0))} </span> </div> </div> ); })} </div> ) : ( <div className="text-center text-on-surface-variant py-md bg-surface-container-low rounded-md border border-outline-variant/50 text-sm"> No scan quotas assigned. </div> )} </div> </div> {/* Billing History */} <div className="bg-surface-container-lowest border border-outline-variant rounded-lg p-lg shadow-sm"> <h2 className="font-headline-md text-headline-md font-bold mb-md flex items-center gap-sm border-b pb-sm"> <span className="material-symbols-outlined text-primary text-[22px]">history</span> Payment History </h2> {loadingBilling ? ( <div className="flex items-center justify-center py-xl text-on-surface-variant font-body-sm"> <span className="material-symbols-outlined animate-spin mr-sm">sync</span> Loading transactions... </div> ) : billingHistory.length === 0 ? ( <div className="text-center py-xl text-on-surface-variant font-body-sm bg-surface-container-low rounded-lg border border-outline-variant/50"> No past transactions found. </div> ) : ( <div className="overflow-x-auto rounded-md border border-outline-variant"> <table className="w-full text-left border-collapse"> <thead> <tr className="bg-surface-container border-b border-outline-variant select-none"> {['Date', 'Plan', 'Amount', 'Status'].map((h, i) => ( <th key={h} onClick={() => handleBillSort(h)} className="p-sm font-label-sm text-on-surface-variant uppercase cursor-pointer hover:bg-surface-container-highest transition-colors group" > <div className="flex items-center gap-xs"> {h} <span className={`material-symbols-outlined text-[14px] opacity-0 group-hover:opacity-50 transition-opacity ${sortBillCol === h ? 'opacity-100 group-hover:opacity-100 text-primary' : ''}`}> {sortBillCol === h && sortBillDir === 'desc' ? 'arrow_downward' : 'arrow_upward'} </span> </div> </th> ))} </tr> </thead> <tbody> {getSortedBilling().map((tx, idx) => ( <tr key={idx} className="border-b border-outline-variant/40 hover:bg-surface-container transition-colors text-sm"> <td className="p-sm text-on-surface">{new Date(tx.created_at).toLocaleDateString()}</td> <td className="p-sm text-on-surface capitalize">{tx.tier_id}</td> <td className="p-sm font-bold text-on-surface">{tx.currency === 'INR' ? '₹' : '$'}{(tx.amount / 100).toFixed(2)}</td> <td className="p-sm"> <span className={`px-xs py-[2px] rounded text-[10px] font-bold uppercase tracking-wider ${tx.status === 'success' ? 'bg-emerald-500/20 text-emerald-500' : 'bg-error-container text-error' }`}> {tx.status} </span> </td> </tr> ))} </tbody> </table> </div> )} </div> </div> )} {/* Profile Tab */} {activeTab === 'profile' && ( <Profile /> )} {/* Team Tab */} {activeTab === 'team' && isTeamAllowed && ( <div className="w-full flex flex-col gap-lg"> <div className="bg-surface-container-lowest border border-outline-variant rounded-xl overflow-hidden shadow-sm"> <div className="p-6 border-b border-outline-variant flex justify-between items-center bg-surface-container/50"> <h2 className="text-xl font-bold flex items-center text-on-surface"> <span className="material-symbols-outlined text-primary mr-2">group</span> Active Team Members </h2> {(user?.role === 'org_admin' || user?.role === 'super_admin') && ( <button onClick={() => setShowAddMember(true)} className="bg-primary hover:bg-primary/90 text-on-primary font-bold py-2 px-4 rounded flex items-center gap-2 transition-colors cursor-pointer border-0 shadow-sm text-sm" > <span className="material-symbols-outlined text-[18px]">person_add</span> Add New Member </button> )} </div> {loadingTeam ? ( <div className="p-10 text-center text-on-surface-variant">Loading team members...</div> ) : ( <div className="overflow-x-auto"> <table className="w-full text-left text-sm"> <thead className="bg-surface-container-low text-on-surface-variant select-none"> <tr> {['Name', 'Email', 'Assigned Role', 'Status', 'Actions'].map((h, i) => ( <th key={h} onClick={() => handleTeamSort(h)} className={`px-6 py-4 font-semibold ${i === 4 ? 'text-right' : ''} ${h !== 'Actions' ? 'cursor-pointer hover:bg-surface-container-highest transition-colors group' : ''}`} > <div className={`flex items-center gap-xs ${i === 4 ? 'justify-end' : ''}`}> {h} {h !== 'Actions' && ( <span className={`material-symbols-outlined text-[16px] opacity-0 group-hover:opacity-50 transition-opacity ${sortTeamCol === h ? 'opacity-100 group-hover:opacity-100 text-primary' : ''}`}> {sortTeamCol === h && sortTeamDir === 'desc' ? 'arrow_downward' : 'arrow_upward'} </span> )} </div> </th> ))} </tr> </thead> <tbody className="divide-y divide-outline-variant"> {getSortedTeam().map((member) => ( <tr key={member.id} className="hover:bg-surface-container/30 transition-colors"> <td className="px-6 py-4 font-medium text-on-surface"> {member.first_name || member.last_name ? `${member.first_name || ''} ${member.last_name || ''}`.trim() : 'N/A'} </td> <td className="px-6 py-4 font-medium text-on-surface">{member.email}</td> <td className="px-6 py-4"> <span className="px-3 py-1 bg-surface-container rounded-full text-xs font-medium text-on-surface-variant capitalize"> {member.role.replace('_', ' ')} </span> </td> <td className="px-6 py-4"> <span className={`px-2 py-1 text-xs font-semibold rounded-full ${member.status === 'Active' ? 'bg-green-500/10 text-green-600' : 'bg-yellow-500/10 text-yellow-600'}`}> {member.status} </span> </td> <td className="px-6 py-4 flex justify-end gap-2"> <button onClick={() => setEditingUser({ ...member, new_password: '' })} className="text-primary hover:bg-primary/10 p-2 rounded transition-colors cursor-pointer border-0 bg-transparent flex items-center justify-center" title="Edit User" > <span className="material-symbols-outlined text-[18px]">edit</span> </button> <button onClick={() => setUserToDelete(member)} className="text-error hover:bg-error/10 p-2 rounded transition-colors cursor-pointer border-0 bg-transparent flex items-center justify-center" title="Remove User" > <span className="material-symbols-outlined text-[18px]">delete</span> </button> </td> </tr> ))} </tbody> </table> </div> )} </div> {/* Add New Member Modal Overlay */} {showAddMember && (user?.role === 'org_admin' || user?.role === 'super_admin') && ( <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4"> <div className="bg-surface-container-lowest border border-outline-variant rounded-xl overflow-hidden shadow-xl max-w-2xl w-full"> <div className="p-6 border-b border-outline-variant bg-surface-container/50 flex justify-between items-center"> <h2 className="text-xl font-bold flex items-center text-on-surface"> <span className="material-symbols-outlined text-primary mr-2">person_add</span> Add New Member </h2> <button onClick={() => setShowAddMember(false)} className="text-on-surface-variant hover:text-on-surface rounded-full p-1 transition-colors cursor-pointer border-0 bg-transparent" > <span className="material-symbols-outlined">close</span> </button> </div> <div className="p-6"> <form onSubmit={handleInviteUser} className="flex flex-col gap-md"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-md"> <div> <label className="block text-sm font-semibold text-on-surface mb-2">First Name</label> <input type="text" value={newUserFirstName} onChange={(e) => setNewUserFirstName(e.target.value)} className="w-full bg-surface border border-outline-variant text-on-surface font-body-md px-4 py-3 rounded-lg focus:border-primary focus:ring-2 focus:ring-primary/20 focus:outline-none transition-all" placeholder="John" /> </div> <div> <label className="block text-sm font-semibold text-on-surface mb-2">Last Name</label> <input type="text" value={newUserLastName} onChange={(e) => setNewUserLastName(e.target.value)} className="w-full bg-surface border border-outline-variant text-on-surface font-body-md px-4 py-3 rounded-lg focus:border-primary focus:ring-2 focus:ring-primary/20 focus:outline-none transition-all" placeholder="Doe" /> </div> </div> <div className="grid grid-cols-1 sm:grid-cols-2 gap-md"> <div> <label className="block text-sm font-semibold text-on-surface mb-2">Email Address</label> <input type="email" required value={newUserEmail} onChange={(e) => setNewUserEmail(e.target.value)} className="w-full bg-surface border border-outline-variant text-on-surface font-body-md px-4 py-3 rounded-lg focus:border-primary focus:ring-2 focus:ring-primary/20 focus:outline-none transition-all" placeholder="user@example.com" /> </div> <div> <label className="block text-sm font-semibold text-on-surface mb-2">Password</label> <div className="relative"> <input type={showNewUserPassword ? "text" : "password"} value={newUserPassword} onChange={(e) => setNewUserPassword(e.target.value)} className="w-full bg-surface border border-outline-variant text-on-surface font-body-md px-4 py-3 pr-12 rounded-lg focus:border-primary focus:ring-2 focus:ring-primary/20 focus:outline-none transition-all" placeholder="Leave blank for auto-generated" /> <button type="button" onClick={() => setShowNewUserPassword(!showNewUserPassword)} className="absolute right-3 top-1/2 -translate-y-1/2 text-on-surface-variant hover:text-on-surface rounded-full p-1 transition-colors bg-transparent border-0 flex items-center justify-center cursor-pointer" > <span className="material-symbols-outlined text-[20px]"> {showNewUserPassword ? 'visibility_off' : 'visibility'} </span> </button> </div> </div> </div> <div className="flex flex-col sm:flex-row justify-between items-end gap-md mt-sm border-t border-outline-variant/50 pt-md"> <div className="flex-1 w-full sm:max-w-[200px]"> <label className="block text-sm font-semibold text-on-surface mb-2">Assign Role</label> <select value={newUserRole} onChange={(e) => setNewUserRole(e.target.value)} className="w-full bg-surface border border-outline-variant text-on-surface font-body-md px-4 py-3 rounded-lg focus:border-primary focus:ring-2 focus:ring-primary/20 focus:outline-none transition-all cursor-pointer" > <option value="soc_analyst">SOC Analyst</option> <option value="executive_user">Executive User</option> </select> </div> <div className="flex gap-sm w-full sm:w-auto"> <button type="button" onClick={() => setShowAddMember(false)} className="w-full sm:w-auto px-6 py-2.5 font-bold rounded-lg border border-outline-variant bg-surface text-on-surface hover:bg-surface-container transition-colors cursor-pointer" > Cancel </button> <button type="submit" disabled={invitingUser} className="w-full sm:w-auto min-w-[150px] bg-primary text-on-primary hover:bg-primary/90 hover:shadow-md font-bold px-6 py-2.5 rounded-lg transition-all cursor-pointer border-0 shadow-sm disabled:opacity-50 flex items-center justify-center" > {invitingUser ? 'Adding...' : 'Confirm & Add'} </button> </div> </div> </form> </div> </div> </div> )} {/* Edit Member Modal Overlay */} {editingUser && (user?.role === 'org_admin' || user?.role === 'super_admin') && ( <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4"> <div className="bg-surface-container-lowest border border-outline-variant rounded-xl overflow-hidden shadow-xl max-w-2xl w-full"> <div className="p-6 border-b border-outline-variant bg-surface-container/50 flex justify-between items-center"> <h2 className="text-xl font-bold flex items-center text-on-surface"> <span className="material-symbols-outlined text-primary mr-2">manage_accounts</span> Edit Member </h2> <button onClick={() => setEditingUser(null)} className="text-on-surface-variant hover:text-on-surface rounded-full p-1 transition-colors cursor-pointer border-0 bg-transparent" > <span className="material-symbols-outlined">close</span> </button> </div> <div className="p-6"> <form onSubmit={handleUpdateUser} className="flex flex-col gap-md"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-md"> <div> <label className="block text-sm font-semibold text-on-surface mb-2">First Name</label> <input type="text" value={editingUser.first_name || ''} onChange={(e) => setEditingUser({ ...editingUser, first_name: e.target.value })} className="w-full bg-surface border border-outline-variant text-on-surface font-body-md px-4 py-3 rounded-lg focus:border-primary focus:ring-2 focus:ring-primary/20 focus:outline-none transition-all" /> </div> <div> <label className="block text-sm font-semibold text-on-surface mb-2">Last Name</label> <input type="text" value={editingUser.last_name || ''} onChange={(e) => setEditingUser({ ...editingUser, last_name: e.target.value })} className="w-full bg-surface border border-outline-variant text-on-surface font-body-md px-4 py-3 rounded-lg focus:border-primary focus:ring-2 focus:ring-primary/20 focus:outline-none transition-all" /> </div> </div> <div className="grid grid-cols-1 sm:grid-cols-2 gap-md"> <div> <label className="block text-sm font-semibold text-on-surface mb-2">Email Address</label> <input type="email" required value={editingUser.email || ''} onChange={(e) => setEditingUser({ ...editingUser, email: e.target.value })} className="w-full bg-surface border border-outline-variant text-on-surface font-body-md px-4 py-3 rounded-lg focus:border-primary focus:ring-2 focus:ring-primary/20 focus:outline-none transition-all" /> </div> <div> <label className="block text-sm font-semibold text-on-surface mb-2">Password</label> <div className="relative"> <input type={showNewUserPassword ? "text" : "password"} value={editingUser.new_password || ''} onChange={(e) => setEditingUser({ ...editingUser, new_password: e.target.value })} className="w-full bg-surface border border-outline-variant text-on-surface font-body-md px-4 py-3 pr-12 rounded-lg focus:border-primary focus:ring-2 focus:ring-primary/20 focus:outline-none transition-all" placeholder="Leave blank to keep unchanged" /> <button type="button" onClick={() => setShowNewUserPassword(!showNewUserPassword)} className="absolute right-3 top-1/2 -translate-y-1/2 text-on-surface-variant hover:text-on-surface rounded-full p-1 transition-colors bg-transparent border-0 flex items-center justify-center cursor-pointer" > <span className="material-symbols-outlined text-[20px]"> {showNewUserPassword ? 'visibility_off' : 'visibility'} </span> </button> </div> </div> </div> <div className="flex flex-col sm:flex-row justify-between items-end gap-md mt-sm border-t border-outline-variant/50 pt-md"> <div className="flex-1 w-full sm:max-w-[200px]"> <label className="block text-sm font-semibold text-on-surface mb-2">Assign Role</label> <select value={editingUser.role} onChange={(e) => setEditingUser({ ...editingUser, role: e.target.value })} className="w-full bg-surface border border-outline-variant text-on-surface font-body-md px-4 py-3 rounded-lg focus:border-primary focus:ring-2 focus:ring-primary/20 focus:outline-none transition-all cursor-pointer" > <option value="soc_analyst">SOC Analyst</option> <option value="executive_user">Executive User</option> </select> </div> <div className="flex gap-sm w-full sm:w-auto"> <button type="button" onClick={() => setEditingUser(null)} className="w-full sm:w-auto px-6 py-2.5 font-bold rounded-lg border border-outline-variant bg-surface text-on-surface hover:bg-surface-container transition-colors cursor-pointer" > Cancel </button> <button type="submit" disabled={updatingUser} className="w-full sm:w-auto min-w-[150px] bg-primary text-on-primary hover:bg-primary/90 hover:shadow-md font-bold px-6 py-2.5 rounded-lg transition-all cursor-pointer border-0 shadow-sm disabled:opacity-50 flex items-center justify-center" > {updatingUser ? 'Saving...' : 'Save Changes'} </button> </div> </div> </form> </div> </div> </div> )} {/* Delete Confirmation Modal */} {userToDelete && ( <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4"> <div className="bg-surface-container-lowest border border-outline-variant rounded-xl overflow-hidden shadow-xl max-w-md w-full"> <div className="p-6"> <div className="flex items-center justify-center w-12 h-12 rounded-full bg-error/10 mb-4 mx-auto"> <span className="material-symbols-outlined text-error text-2xl">warning</span> </div> <h3 className="text-xl font-bold text-center text-on-surface mb-2">Remove Team Member?</h3> <p className="text-center text-on-surface-variant mb-6"> Are you sure you want to remove <strong>{userToDelete.email}</strong> from the organization? This action cannot be undone and they will lose all access immediately. </p> <div className="flex gap-sm w-full"> <button onClick={() => setUserToDelete(null)} className="w-1/2 px-6 py-2.5 font-bold rounded-lg border border-outline-variant bg-surface text-on-surface hover:bg-surface-container transition-colors cursor-pointer" > Cancel </button> <button onClick={executeDeleteUser} disabled={deletingUser} className="w-1/2 bg-error text-white hover:bg-error/90 hover:shadow-md font-bold px-6 py-2.5 rounded-lg transition-all cursor-pointer border-0 shadow-sm disabled:opacity-50 flex items-center justify-center" > {deletingUser ? 'Removing...' : 'Remove User'} </button> </div> </div> </div> </div> )} </div> )} {/* API Keys Tab (Placeholder) */} {activeTab === 'apiKeys' && isApiKeysAllowed && ( <div className="w-full"> <div className="bg-surface-container-lowest border border-outline-variant rounded-lg shadow-sm mb-lg overflow-hidden relative min-h-[400px] flex items-center justify-center"> {/* Background decorative elements */} <div className="absolute top-0 left-0 w-full h-full overflow-hidden pointer-events-none"> <div className="absolute -top-[20%] -right-[10%] w-[50%] h-[60%] bg-primary/5 rounded-full blur-3xl"></div> <div className="absolute -bottom-[20%] -left-[10%] w-[40%] h-[50%] bg-primary/5 rounded-full blur-3xl"></div> {/* Subtle grid pattern overlay */} <div className="absolute inset-0" style={{ backgroundImage: 'radial-gradient(var(--tw-colors-outline-variant) 1px, transparent 1px)', backgroundSize: '32px 32px', opacity: 0.1 }}></div> </div> <div className="relative z-10 flex flex-col items-center text-center max-w-lg mx-auto p-8"> <div className="mb-6 relative"> <div className="absolute inset-0 bg-primary/20 blur-xl rounded-full"></div> <div className="h-20 w-20 rounded-2xl bg-gradient-to-br from-primary to-primary/80 flex items-center justify-center text-on-primary shadow-lg shadow-primary/30 relative z-10 border border-white/20"> <span className="material-symbols-outlined text-[40px]">vpn_key</span> </div> </div> <span className="bg-surface-variant text-on-surface-variant text-[10px] uppercase tracking-widest font-bold px-3 py-1 rounded-full mb-4 border border-outline-variant"> Coming Soon </span> <h2 className="font-headline-md text-on-surface font-bold mb-3 text-3xl"> Developer API Access </h2> <p className="font-body-md text-on-surface-variant leading-relaxed mb-8"> We are actively building a robust, high-performance API for LarShield. Soon, you will be able to programmatically manage scans, retrieve security reports, and integrate seamlessly with your CI/CD pipelines. </p> <button type="button" onClick={() => toast.success("You have been added to the early access waitlist!")} className="bg-surface-container hover:bg-surface-container-high text-on-surface border border-outline-variant px-6 py-3 rounded-lg font-bold transition-all shadow-sm flex items-center gap-2 cursor-pointer active:scale-95" > <span className="material-symbols-outlined text-[18px]">notifications_active</span> Notify Me When Live </button> </div> </div> </div> )} {/* Reschedule Demo Call Modal */} {rescheduleModal.isOpen && ( <div className="fixed inset-0 z-[9999] flex items-center justify-center p-4"> <div className="fixed inset-0 bg-slate-900/60 backdrop-blur-sm" onClick={() => setRescheduleModal({ ...rescheduleModal, isOpen: false })}></div> <div className="relative bg-white dark:bg-slate-900 rounded-2xl border border-slate-200 dark:border-slate-800 shadow-2xl max-w-md w-full p-6 z-10 animate-slide-up text-left font-sans"> <div className="flex justify-between items-center pb-3 border-b border-slate-100 dark:border-slate-800"> <h3 className="text-lg font-bold text-slate-900 dark:text-white flex items-center gap-2"> <span className="material-symbols-outlined text-primary">edit_calendar</span> Reschedule Demo Call </h3> <button onClick={() => setRescheduleModal({ ...rescheduleModal, isOpen: false })} className="text-slate-400 hover:text-slate-600 border-0 bg-transparent cursor-pointer p-1"> <span className="material-symbols-outlined text-[20px]">close</span> </button> </div> <form onSubmit={handleSaveReschedule} className="mt-4 flex flex-col gap-4"> <div> <label className="text-[11px] font-bold text-slate-500 uppercase tracking-wider block mb-1">Lead Email</label> <input type="text" disabled value={rescheduleModal.email} className="w-full bg-slate-100 border border-slate-200 rounded-lg p-2.5 text-xs text-slate-700 font-bold" /> </div> <div> <label className="text-xs font-bold text-slate-700 dark:text-slate-300 block mb-1">New Meeting Date</label> <input type="text" required placeholder="e.g. August 28, 2026" value={rescheduleModal.meetingDate} onChange={(e) => setRescheduleModal({ ...rescheduleModal, meetingDate: e.target.value })} className="w-full border border-slate-300 rounded-lg p-2.5 text-xs text-slate-900 dark:text-white dark:bg-slate-800 focus:outline-none focus:border-primary" /> </div> <div> <label className="text-xs font-bold text-slate-700 dark:text-slate-300 block mb-1">New Meeting Time</label> <input type="text" required placeholder="e.g. 04:00 PM" value={rescheduleModal.meetingTime} onChange={(e) => setRescheduleModal({ ...rescheduleModal, meetingTime: e.target.value })} className="w-full border border-slate-300 rounded-lg p-2.5 text-xs text-slate-900 dark:text-white dark:bg-slate-800 focus:outline-none focus:border-primary" /> </div> <div> <label className="text-xs font-bold text-slate-700 dark:text-slate-300 block mb-1">Update Status</label> <select value={rescheduleModal.status} onChange={(e) => setRescheduleModal({ ...rescheduleModal, status: e.target.value })} className="w-full border border-slate-300 rounded-lg p-2.5 text-xs text-slate-900 dark:text-white dark:bg-slate-800 focus:outline-none focus:border-primary cursor-pointer" > <option value="rescheduled">Rescheduled</option> <option value="pending">Pending</option> <option value="completed">Completed / Conducted</option> <option value="cancelled">Cancelled / Not Conducted</option> </select> </div> <div className="flex justify-end gap-2 pt-3 border-t border-slate-100 dark:border-slate-800"> <button type="button" onClick={() => setRescheduleModal({ ...rescheduleModal, isOpen: false })} className="px-4 py-2 rounded-lg text-xs font-bold text-slate-600 border border-slate-300 hover:bg-slate-100 cursor-pointer bg-white" > Cancel </button> <button type="submit" className="px-4 py-2 rounded-lg text-xs font-bold text-white bg-primary hover:brightness-110 shadow-sm cursor-pointer border-0" > Save & Update </button> </div> </form> </div> </div> )} </div> ); };
import { useState, useEffect } from 'react';
import { useAuth } from '../components/AuthContext';
import toast from 'react-hot-toast';
import Profile from './Profile';
export const AlertSettingsPage = () => {
const { token, user, reloadUser } = useAuth();
const [activeTab, setActiveTab] = useState(() => {
return localStorage.getItem('settingsActiveTab') || 'profile';
}); // profile, notifications, apiKeys, team, billing, scheduler
useEffect(() => {
localStorage.setItem('settingsActiveTab', activeTab);
}, [activeTab]);
// Notification states (connected to backend)
const [emailNotifications, setEmailNotifications] = useState(true);
const [webhookUrl, setWebhookUrl] = useState('');
const [severityThreshold, setSeverityThreshold] = useState('Medium');
const [reportLogoUrl, setReportLogoUrl] = useState('');
const [teamUsers, setTeamUsers] = useState([]);
const [loadingTeam, setLoadingTeam] = useState(false);
const [newUserFirstName, setNewUserFirstName] = useState('');
const [newUserLastName, setNewUserLastName] = useState('');
const [newUserEmail, setNewUserEmail] = useState('');
const [newUserPassword, setNewUserPassword] = useState('');
const [newUserRole, setNewUserRole] = useState('soc_analyst');
const [invitingUser, setInvitingUser] = useState(false);
const [showAddMember, setShowAddMember] = useState(false);
const [showNewUserPassword, setShowNewUserPassword] = useState(false);
const [editingUser, setEditingUser] = useState(null);
const [updatingUser, setUpdatingUser] = useState(false);
const [userToDelete, setUserToDelete] = useState(null);
const [deletingUser, setDeletingUser] = useState(false);
const [passwordData, setPasswordData] = useState({ currentPassword: '', newPassword: '', confirmPassword: '' });
const [passwordStatus, setPasswordStatus] = useState({ loading: false, error: null, success: false });
const [showPassword, setShowPassword] = useState({ current: false, new: false, confirm: false });
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [message, setMessage] = useState('');
const [error, setError] = useState('');
const [billingHistory, setBillingHistory] = useState([]);
const [loadingBilling, setLoadingBilling] = useState(false);
const [scanQuotas, setScanQuotas] = useState([]);
const [loadingQuotas, setLoadingQuotas] = useState(false);
// Demo Bookings State
const [demoBookings, setDemoBookings] = useState([]);
const [loadingDemoBookings, setLoadingDemoBookings] = useState(false);
const [rescheduleModal, setRescheduleModal] = useState({
isOpen: false,
bookingId: null,
email: '',
meetingDate: '',
meetingTime: '',
status: 'rescheduled'
});
const handleUpdateDemoBookingStatus = async (bookingId, status) => {
try {
const res = await fetch(`/api/demo/bookings/${bookingId}`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ status })
});
if (res.ok) {
toast.success(`Booking status updated to ${status}`);
fetchDemoBookings();
} else {
toast.error("Failed to update status");
}
} catch (err) {
toast.error("Error updating status");
}
};
const handleOpenReschedule = (booking) => {
setRescheduleModal({
isOpen: true,
bookingId: booking.id,
email: booking.email,
meetingDate: booking.meeting_date || '',
meetingTime: booking.meeting_time || '',
status: 'rescheduled'
});
};
const handleSaveReschedule = async (e) => {
e.preventDefault();
try {