SentinelScan-WSS / frontend /src /pages /Settings.jsx
larxius's picture
Update frontend/src/pages/Settings.jsx
34c09d6 verified
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.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>
);
};