HR-ATS-Portal/frontend/src/screens/Settings.jsx

820 lines
33 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/* ============================================================
Settings — org settings tabs persist via GET/PUT /org-settings/*.
Users + Appearance stay as before. Email Templates stay decorative
(explicitly out of Section C wiring scope). Roles/Permissions remain
chrome; Access Control is the authoritative RBAC surface.
============================================================ */
import { useEffect, useRef, useState } from 'react'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import Modal from '../ui/Modal'
import { Tabs } from '../ui/Tabs'
import { Avatar, Badge, EmptyState, FieldError, Icon } from '../ui/primitives'
import { useToast } from '../ui/Toast'
import { useTheme } from '../theme/ThemeProvider'
import { useFormState } from '../components/AuthLayout'
import { usePermission } from '../auth/AuthContext'
import { qk } from '../lib/queryKeys'
import { friendlyAuthError } from '../lib/errors'
import * as rolesApi from '../api/roles'
import * as usersApi from '../api/users'
import * as orgSettingsApi from '../api/orgSettings'
import { roles as seedRoles } from '../data/seed'
const TABS = [
'General', 'Users', 'Roles', 'Permissions', 'Notifications',
'Email Templates', 'Career Portal', 'Branding', 'Security', 'Appearance',
]
const ORG_TABS = new Set(['General', 'Notifications', 'Career Portal', 'Branding', 'Security'])
const CATEGORY_BY_TAB = {
General: 'general',
Notifications: 'notifications',
'Career Portal': 'career_portal',
Branding: 'branding',
Security: 'security',
}
function ToggleRow({ title, desc, checked, onChange, disabled }) {
return (
<div className="setting-row">
<div className="setting-info"><h4>{title}</h4><p>{desc}</p></div>
<label className="switch">
<input
type="checkbox"
checked={Boolean(checked)}
disabled={disabled}
onChange={(e) => onChange?.(e.target.checked)}
/>
<span className="switch-track" />
</label>
</div>
)
}
function useOrgDraft(category, defaults) {
const query = useQuery({
queryKey: qk.orgSettings.list({ category }),
queryFn: async () => orgSettingsApi.toMap(await orgSettingsApi.list({ category })),
})
const [draft, setDraft] = useState(defaults)
useEffect(() => {
if (!query.data) return
setDraft((prev) => {
const next = { ...prev }
for (const key of Object.keys(defaults)) {
if (query.data[key] !== undefined) next[key] = query.data[key]
}
return next
})
}, [query.data]) // eslint-disable-line react-hooks/exhaustive-deps
function setField(key, value) {
setDraft((d) => ({ ...d, [key]: value }))
}
function toPayload() {
return Object.entries(draft).map(([key, value]) => ({ key, value, category }))
}
return { query, draft, setField, toPayload }
}
export default function Settings() {
const { toast } = useToast()
const { can } = usePermission()
const qc = useQueryClient()
const [tab, setTab] = useState('General')
const saveRef = useRef(null)
const save = useMutation({
mutationFn: async () => {
if (!saveRef.current) return null
return saveRef.current()
},
onSuccess: () => {
qc.invalidateQueries({ queryKey: qk.orgSettings.all() })
toast('Settings saved', 'success')
},
onError: (err) => toast(friendlyAuthError(err, 'Could not save settings.'), 'error'),
})
const canConfigure = can('settings.configure')
const showOrgSave = ORG_TABS.has(tab)
return (
<div className="page">
<div className="page-head">
<div>
<h1 className="page-title">Settings</h1>
<p className="page-sub">Configure your workspace and team preferences</p>
</div>
<div className="page-head-actions">
{showOrgSave && (
<button
className="btn btn-primary"
disabled={!canConfigure || save.isPending}
onClick={() => save.mutate()}
>
<Icon name="check" /> {save.isPending ? 'Saving…' : 'Save Changes'}
</button>
)}
</div>
</div>
<Tabs value={tab} onChange={setTab} tabs={TABS.map((t) => ({ key: t, label: t }))} />
<div className="tab-pane active">
{tab === 'General' && <General registerSave={(fn) => { saveRef.current = fn }} />}
{tab === 'Users' && <Users />}
{tab === 'Roles' && <Roles />}
{tab === 'Permissions' && <Permissions />}
{tab === 'Notifications' && <Notifications registerSave={(fn) => { saveRef.current = fn }} />}
{tab === 'Email Templates' && <EmailTemplates />}
{tab === 'Career Portal' && <CareerPortal registerSave={(fn) => { saveRef.current = fn }} />}
{tab === 'Branding' && <Branding registerSave={(fn) => { saveRef.current = fn }} />}
{tab === 'Security' && <Security registerSave={(fn) => { saveRef.current = fn }} />}
{tab === 'Appearance' && <Appearance />}
</div>
</div>
)
}
function General({ registerSave }) {
const defaults = {
'general.company_name': 'Utopia Brands Inc.',
'general.website': 'https://utopiabrands.com',
'general.industry': 'Consumer Goods',
'general.company_size': '201500',
'general.timezone': '(GMT-08:00) Pacific Time',
'general.currency': 'USD ($)',
'general.auto_archive_stale_jobs': true,
'general.duplicate_detection': true,
}
const { query, draft, setField, toPayload } = useOrgDraft(CATEGORY_BY_TAB.General, defaults)
useEffect(() => {
registerSave?.(() => orgSettingsApi.update(toPayload()))
})
if (query.isPending) {
return <div className="card"><div className="card-body"><EmptyState icon="settings" title="Loading…">Fetching organisation settings.</EmptyState></div></div>
}
if (query.isError) {
return (
<div className="card"><div className="card-body">
<EmptyState icon="settings" title="Couldnt load settings">
{friendlyAuthError(query.error, 'This tab needs settings.view.')}
</EmptyState>
</div></div>
)
}
return (
<div className="card">
<div className="card-body">
<div className="form-grid">
<div className="form-field">
<label>Organization Name</label>
<input value={draft['general.company_name'] ?? ''} onChange={(e) => setField('general.company_name', e.target.value)} />
</div>
<div className="form-field">
<label>Company Website</label>
<input value={draft['general.website'] ?? ''} onChange={(e) => setField('general.website', e.target.value)} />
</div>
<div className="form-field">
<label>Industry</label>
<select value={draft['general.industry'] ?? ''} onChange={(e) => setField('general.industry', e.target.value)}>
<option>Consumer Goods</option><option>Technology</option><option>Retail</option>
</select>
</div>
<div className="form-field">
<label>Company Size</label>
<select value={draft['general.company_size'] ?? ''} onChange={(e) => setField('general.company_size', e.target.value)}>
<option>201500</option><option>51200</option><option>500+</option>
</select>
</div>
<div className="form-field">
<label>Default Time Zone</label>
<select value={draft['general.timezone'] ?? ''} onChange={(e) => setField('general.timezone', e.target.value)}>
<option>(GMT-08:00) Pacific Time</option>
<option>(GMT-05:00) Eastern Time</option>
<option>(GMT+00:00) UTC</option>
</select>
</div>
<div className="form-field">
<label>Default Currency</label>
<select value={draft['general.currency'] ?? ''} onChange={(e) => setField('general.currency', e.target.value)}>
<option>USD ($)</option><option>EUR ()</option><option>GBP (£)</option>
</select>
</div>
</div>
<div className="divider" />
<ToggleRow
title="Auto-archive stale jobs"
desc="Automatically close requisitions inactive for 90 days"
checked={draft['general.auto_archive_stale_jobs']}
onChange={(v) => setField('general.auto_archive_stale_jobs', v)}
/>
<ToggleRow
title="Duplicate detection"
desc="Flag candidates that already exist in the system"
checked={draft['general.duplicate_detection']}
onChange={(v) => setField('general.duplicate_detection', v)}
/>
</div>
</div>
)
}
/** Real data: GET /users/fetch (requires rbac_users.view). */
function Users() {
const { toast } = useToast()
const [editing, setEditing] = useState(null)
const usersQuery = useQuery({
queryKey: qk.users.list(),
queryFn: () => usersApi.list({ top: 50 }).then((r) => r.data ?? []),
})
const users = usersQuery.data ?? []
return (
<div className="card">
<div className="card-head">
<div>
<h3>Team Members</h3>
<span className="ch-sub">
{usersQuery.isPending ? 'Loading…' : `${users.length} users`}
</span>
</div>
<button className="btn btn-primary btn-sm" onClick={() => toast('Invite sent', 'success')}>
<Icon name="plus" /> Invite User
</button>
</div>
{usersQuery.isError ? (
<div className="card-body">
<div className="empty-state">
<Icon name="alert" />
<h3>Couldnt load users</h3>
<p>
{friendlyAuthError(usersQuery.error, 'The server did not return the user list.')}
{' '}This tab needs the <code>rbac_users.view</code> permission.
</p>
</div>
</div>
) : (
<div className="table-wrap">
<table className="data">
<thead>
<tr>
<th>User</th><th>Role</th><th>Status</th><th>Created</th>
<th style={{ textAlign: 'right' }}>Actions</th>
</tr>
</thead>
<tbody>
{users.map((u) => (
<tr key={u.id}>
<td>
<div className="user-cell">
<Avatar name={u.name} />
<div>
<div className="cell-primary">{u.name}</div>
<div className="cell-sub">{u.email}</div>
</div>
</div>
</td>
<td><Badge className="b-indigo">{u.role_name || 'No role'}</Badge></td>
<td><Badge>{u.is_active ? 'Active' : 'Pending'}</Badge></td>
<td className="text-muted">{u.created_at ? String(u.created_at).slice(0, 10) : '—'}</td>
<td style={{ textAlign: 'right' }}>
<div className="row-actions">
<button
className="act-btn"
onClick={() => setEditing(u)}
aria-label={`Edit ${u.name}`}
>
<Icon name="edit" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{editing && (
<AssignRoleModal user={editing} users={users} onClose={() => setEditing(null)} />
)}
</div>
)
}
function AssignRoleModal({ user, users, onClose }) {
const { toast } = useToast()
const { can } = usePermission()
const qc = useQueryClient()
const form = useFormState({
user_id: String(user.id),
role_id: user.role_id == null ? '' : String(user.role_id),
})
const rolesQuery = useQuery({
queryKey: qk.roles.list(),
queryFn: () => rolesApi.listRoles().then((r) => r.data ?? []),
})
const roles = (rolesQuery.data ?? []).filter((r) => r.is_active)
const target = users.find((u) => String(u.id) === form.values.user_id) ?? user
const currentRoleId = target.role_id == null ? '' : String(target.role_id)
const clearing = form.values.role_id === ''
const dirty = form.values.role_id !== currentRoleId
function pickUser(id) {
const next = users.find((u) => String(u.id) === id)
form.setValues({
user_id: id,
role_id: next?.role_id == null ? '' : String(next.role_id),
})
}
const save = useMutation({
mutationFn: () =>
clearing
? usersApi.removeRole(form.values.user_id)
: usersApi.assignRole(form.values.user_id, Number(form.values.role_id)),
onSuccess: () => {
qc.invalidateQueries({ queryKey: qk.users.all() })
const picked = roles.find((r) => String(r.id) === form.values.role_id)
toast(
clearing
? `Role removed from ${target.name}`
: `${target.name} is now ${picked?.role_name ?? 'assigned'}`,
'success',
)
onClose()
},
onError: (err) => toast(friendlyAuthError(err, 'Could not update the role.'), 'error'),
})
const busy = save.isPending
function submit() {
if (!dirty) {
onClose()
return
}
save.mutate()
}
return (
<Modal
title="Assign Role"
subtitle={`Change which role ${target.name} holds`}
onClose={onClose}
footer={
<>
<button className="btn btn-secondary" onClick={onClose} disabled={busy}>Cancel</button>
<button
className="btn btn-primary"
onClick={submit}
disabled={busy || rolesQuery.isPending}
>
<Icon name="check" /> {busy ? 'Saving…' : 'Save Changes'}
</button>
</>
}
>
<form noValidate onSubmit={(e) => { e.preventDefault(); submit() }}>
<div className="form-grid">
<div className="form-field col-span-2">
<label htmlFor="ar-user">User</label>
<select
id="ar-user"
value={form.values.user_id}
onChange={(e) => pickUser(e.target.value)}
disabled={busy}
>
{users.map((u) => (
<option key={u.id} value={u.id}>{u.name} {u.email}</option>
))}
</select>
</div>
<div className="form-field col-span-2">
<label htmlFor="ar-role">Role</label>
<select
id="ar-role"
className={rolesQuery.isError ? 'err' : ''}
value={form.values.role_id}
onChange={(e) => form.setField('role_id', e.target.value)}
disabled={busy || rolesQuery.isPending || rolesQuery.isError}
>
<option value="">No role</option>
{roles.map((r) => (
<option key={r.id} value={r.id}>{r.role_name}</option>
))}
</select>
<FieldError>
{rolesQuery.isError
? friendlyAuthError(rolesQuery.error, 'Could not load the role list.')
: null}
</FieldError>
{rolesQuery.isPending && <span className="lr-sub">Loading roles</span>}
{clearing && currentRoleId !== '' && (
<span className="lr-sub">
Saving will clear this users role. They keep no permissions until reassigned.
</span>
)}
</div>
</div>
{!can('rbac_users.manage') && (
<div className="alert alert-danger" style={{ marginTop: 14 }}>
Your account does not hold <code>rbac_users.manage</code>, which the server requires on
top of <code>rbac_users.edit</code> to change a role. Saving will be rejected.
</div>
)}
</form>
</Modal>
)
}
function Roles() {
const { toast } = useToast()
return (
<div className="card">
<div className="card-head">
<div><h3>Roles</h3><span className="ch-sub">Define access levels use Access Control for live RBAC</span></div>
<button className="btn btn-primary btn-sm" onClick={() => toast('Use Access Control to manage roles', 'info')}>
<Icon name="plus" /> Add Role
</button>
</div>
<div className="card-body">
<div className="list-tight">
{seedRoles.map((r) => (
<div className="list-row" key={r.name}>
<span className="kpi-icn i-purple" style={{ width: 40, height: 40, borderRadius: 11 }}>
<Icon name="users" />
</span>
<div className="lr-main"><div className="lr-title">{r.name}</div><div className="lr-sub">{r.desc}</div></div>
<div className="lr-right"><div className="fw-600">{r.users} users</div><div className="lr-sub">{r.perms}</div></div>
<button className="act-btn" onClick={() => toast(`Editing ${r.name} role`, 'info')}>
<Icon name="edit" />
</button>
</div>
))}
</div>
</div>
</div>
)
}
function Permissions() {
const modules = ['Jobs', 'Candidates', 'Interviews', 'Offers', 'Reports', 'Settings']
const perms = ['View', 'Create', 'Edit', 'Delete']
const { toast } = useToast()
return (
<div className="card">
<div className="card-head">
<div><h3>Permission Matrix</h3><span className="ch-sub">Recruiter role</span></div>
<select className="select"><option>Recruiter</option><option>Hiring Manager</option><option>Administrator</option></select>
</div>
<p className="text-muted text-sm" style={{ padding: '0 18px' }}>
This is a simplified view. The authoritative matrix 13 modules × 8 actions, resolved from the
server lives on <b>Access Control</b>.
</p>
<div className="table-wrap">
<table className="data">
<thead>
<tr><th>Module</th>{perms.map((p) => <th style={{ textAlign: 'center' }} key={p}>{p}</th>)}</tr>
</thead>
<tbody>
{modules.map((m) => (
<tr key={m}>
<td className="cell-primary">{m}</td>
{perms.map((p) => (
<td style={{ textAlign: 'center' }} key={p}>
<label className="switch">
<input
type="checkbox"
defaultChecked={m !== 'Settings'}
onChange={() => toast('Use Access Control to change permissions', 'info')}
/>
<span className="switch-track" />
</label>
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>
)
}
function Notifications({ registerSave }) {
const defaults = {
'notifications.email_new_applications': true,
'notifications.email_interview_reminders': true,
'notifications.email_offer_responses': true,
'notifications.email_weekly_digest': false,
'notifications.inapp_mentions': true,
'notifications.inapp_stage_changes': false,
'notifications.inapp_task_assignments': true,
}
const { query, draft, setField, toPayload } = useOrgDraft(CATEGORY_BY_TAB.Notifications, defaults)
useEffect(() => {
registerSave?.(() => orgSettingsApi.update(toPayload()))
})
if (query.isPending) {
return <div className="card"><div className="card-body"><EmptyState icon="bell" title="Loading…">Fetching notification preferences.</EmptyState></div></div>
}
if (query.isError) {
return (
<div className="card"><div className="card-body">
<EmptyState icon="bell" title="Couldnt load preferences">
{friendlyAuthError(query.error, 'This tab needs settings.view.')}
</EmptyState>
</div></div>
)
}
return (
<div className="card">
<div className="card-body">
<div className="form-section-title" style={{ marginTop: 0 }}>Email Notifications</div>
<ToggleRow title="New applications" desc="Get notified when a candidate applies" checked={draft['notifications.email_new_applications']} onChange={(v) => setField('notifications.email_new_applications', v)} />
<ToggleRow title="Interview reminders" desc="Reminders 30 minutes before interviews" checked={draft['notifications.email_interview_reminders']} onChange={(v) => setField('notifications.email_interview_reminders', v)} />
<ToggleRow title="Offer responses" desc="When candidates accept or decline offers" checked={draft['notifications.email_offer_responses']} onChange={(v) => setField('notifications.email_offer_responses', v)} />
<ToggleRow title="Weekly digest" desc="A summary of hiring activity every Monday" checked={draft['notifications.email_weekly_digest']} onChange={(v) => setField('notifications.email_weekly_digest', v)} />
<div className="form-section-title">In-App Notifications</div>
<ToggleRow title="Mentions" desc="When a teammate @mentions you" checked={draft['notifications.inapp_mentions']} onChange={(v) => setField('notifications.inapp_mentions', v)} />
<ToggleRow title="Stage changes" desc="When a candidate moves stages" checked={draft['notifications.inapp_stage_changes']} onChange={(v) => setField('notifications.inapp_stage_changes', v)} />
<ToggleRow title="Task assignments" desc="When you are assigned a task" checked={draft['notifications.inapp_task_assignments']} onChange={(v) => setField('notifications.inapp_task_assignments', v)} />
</div>
</div>
)
}
function EmailTemplates() {
return (
<div className="card">
<div className="card-body">
<EmptyState icon="mail" title="Email templates not wired">
Template CRUD exists on the backend but is out of scope for this wiring pass.
Use Access Control / org settings for other configuration.
</EmptyState>
</div>
</div>
)
}
function CareerPortal({ registerSave }) {
const defaults = {
'career_portal.url': 'https://careers.utopiabrands.com',
'career_portal.headline': 'Build the future with us',
'career_portal.cta': 'View Open Roles',
'career_portal.public_job_board': true,
'career_portal.one_click_apply': true,
'career_portal.show_salary': false,
'career_portal.enable_referrals': true,
}
const { query, draft, setField, toPayload } = useOrgDraft(CATEGORY_BY_TAB['Career Portal'], defaults)
useEffect(() => {
registerSave?.(() => orgSettingsApi.update(toPayload()))
})
if (query.isPending) {
return <div className="card"><div className="card-body"><EmptyState icon="settings" title="Loading…">Fetching career portal settings.</EmptyState></div></div>
}
if (query.isError) {
return (
<div className="card"><div className="card-body">
<EmptyState icon="settings" title="Couldnt load settings">
{friendlyAuthError(query.error, 'This tab needs settings.view.')}
</EmptyState>
</div></div>
)
}
return (
<div className="card">
<div className="card-body">
<div className="form-grid">
<div className="form-field col-span-2">
<label>Careers Page URL</label>
<input value={draft['career_portal.url'] ?? ''} onChange={(e) => setField('career_portal.url', e.target.value)} />
</div>
<div className="form-field">
<label>Page Headline</label>
<input value={draft['career_portal.headline'] ?? ''} onChange={(e) => setField('career_portal.headline', e.target.value)} />
</div>
<div className="form-field">
<label>Primary CTA Text</label>
<input value={draft['career_portal.cta'] ?? ''} onChange={(e) => setField('career_portal.cta', e.target.value)} />
</div>
</div>
<div className="divider" />
<ToggleRow title="Public job board" desc="Make open roles visible to the public" checked={draft['career_portal.public_job_board']} onChange={(v) => setField('career_portal.public_job_board', v)} />
<ToggleRow title="Allow one-click apply" desc="Let candidates apply with LinkedIn" checked={draft['career_portal.one_click_apply']} onChange={(v) => setField('career_portal.one_click_apply', v)} />
<ToggleRow title="Show salary ranges" desc="Display compensation on job listings" checked={draft['career_portal.show_salary']} onChange={(v) => setField('career_portal.show_salary', v)} />
<ToggleRow title="Enable referrals" desc="Employees can refer candidates" checked={draft['career_portal.enable_referrals']} onChange={(v) => setField('career_portal.enable_referrals', v)} />
</div>
</div>
)
}
function Branding({ registerSave }) {
const colors = ['#004d43', '#ceff71', '#25e9a5', '#8e92ff', '#1a3134', '#eafff4']
const defaults = {
'branding.primary_color': '#004d43',
'branding.email_footer': 'Utopia Brands · San Francisco, CA',
'branding.support_email': 'talent@utopiabrands.com',
}
const { query, draft, setField, toPayload } = useOrgDraft(CATEGORY_BY_TAB.Branding, defaults)
useEffect(() => {
registerSave?.(() => orgSettingsApi.update(toPayload()))
})
if (query.isPending) {
return <div className="card"><div className="card-body"><EmptyState icon="settings" title="Loading…">Fetching branding settings.</EmptyState></div></div>
}
if (query.isError) {
return (
<div className="card"><div className="card-body">
<EmptyState icon="settings" title="Couldnt load settings">
{friendlyAuthError(query.error, 'This tab needs settings.view.')}
</EmptyState>
</div></div>
)
}
return (
<div className="card">
<div className="card-body">
<div className="setting-row">
<div className="setting-info"><h4>Brand Color</h4><p>Primary accent across the portal (persisted; not yet applied globally)</p></div>
<div className="flex items-center gap-8">
{colors.map((c) => (
<span
key={c}
role="button"
tabIndex={0}
style={{
width: 28, height: 28, borderRadius: 8, background: c, cursor: 'pointer',
border: draft['branding.primary_color'] === c ? '2px solid var(--primary)' : '2px solid var(--border)',
}}
onClick={() => setField('branding.primary_color', c)}
onKeyDown={(e) => { if (e.key === 'Enter') setField('branding.primary_color', c) }}
/>
))}
</div>
</div>
<div className="form-grid" style={{ marginTop: 16 }}>
<div className="form-field">
<label>Email Footer</label>
<input value={draft['branding.email_footer'] ?? ''} onChange={(e) => setField('branding.email_footer', e.target.value)} />
</div>
<div className="form-field">
<label>Support Email</label>
<input value={draft['branding.support_email'] ?? ''} onChange={(e) => setField('branding.support_email', e.target.value)} />
</div>
</div>
</div>
</div>
)
}
function Security({ registerSave }) {
const defaults = {
'security.two_factor_enabled': true,
'security.sso_enabled': false,
'security.ip_allowlist': false,
'security.audit_logging': true,
'security.session_timeout': '30 minutes',
'security.password_policy': 'Strong (12+ chars)',
'security.data_retention_months': '24 months',
}
const { query, draft, setField, toPayload } = useOrgDraft(CATEGORY_BY_TAB.Security, defaults)
useEffect(() => {
registerSave?.(() => orgSettingsApi.update(toPayload()))
})
if (query.isPending) {
return <div className="card"><div className="card-body"><EmptyState icon="shield" title="Loading…">Fetching security settings.</EmptyState></div></div>
}
if (query.isError) {
return (
<div className="card"><div className="card-body">
<EmptyState icon="shield" title="Couldnt load settings">
{friendlyAuthError(query.error, 'This tab needs settings.view.')}
</EmptyState>
</div></div>
)
}
return (
<div className="card">
<div className="card-body">
<div className="alert alert-danger" style={{ marginBottom: 16 }}>
<b>Not yet enforced.</b> These controls persist flags only none of them is wired to
enforcement. The backend today has no 2FA, no SSO, no IP allowlist and no audit log.
Do not read the toggles below as a statement of what is switched on.
</div>
<ToggleRow title="Two-factor authentication" desc="Require 2FA for all team members" checked={draft['security.two_factor_enabled']} onChange={(v) => setField('security.two_factor_enabled', v)} />
<ToggleRow title="Single Sign-On (SSO)" desc="Enable SAML-based SSO login" checked={draft['security.sso_enabled']} onChange={(v) => setField('security.sso_enabled', v)} />
<ToggleRow title="IP allowlist" desc="Restrict access to approved IP ranges" checked={draft['security.ip_allowlist']} onChange={(v) => setField('security.ip_allowlist', v)} />
<ToggleRow title="Audit logging" desc="Track all data access and changes" checked={draft['security.audit_logging']} onChange={(v) => setField('security.audit_logging', v)} />
<div className="form-grid" style={{ marginTop: 16 }}>
<div className="form-field">
<label>Session Timeout</label>
<select value={draft['security.session_timeout'] ?? ''} onChange={(e) => setField('security.session_timeout', e.target.value)}>
<option>30 minutes</option><option>1 hour</option><option>8 hours</option>
</select>
</div>
<div className="form-field">
<label>Password Policy</label>
<select value={draft['security.password_policy'] ?? ''} onChange={(e) => setField('security.password_policy', e.target.value)}>
<option>Strong (12+ chars)</option><option>Medium (8+ chars)</option>
</select>
</div>
</div>
<div className="divider" />
<div className="setting-row">
<div className="setting-info"><h4>Data Retention</h4><p>Auto-delete candidate data after set period</p></div>
<select className="select" value={draft['security.data_retention_months'] ?? ''} onChange={(e) => setField('security.data_retention_months', e.target.value)}>
<option>24 months</option><option>12 months</option><option>36 months</option>
</select>
</div>
</div>
</div>
)
}
function Appearance() {
const { toast } = useToast()
const { setTheme, useSystemTheme } = useTheme()
function pick(mode) {
if (mode === 'system') useSystemTheme()
else setTheme(mode)
toast(`Theme updated to ${mode}`, 'success')
}
return (
<div className="card">
<div className="card-body">
<div className="form-section-title" style={{ marginTop: 0 }}>Theme</div>
<div className="grid g-3" style={{ marginBottom: 8 }}>
<div className="card theme-opt" style={{ cursor: 'pointer', overflow: 'hidden' }} onClick={() => pick('light')}>
<div style={{ height: 80, background: '#f1f7f4', borderBottom: '1px solid var(--border)', display: 'flex' }}>
<div style={{ width: '30%', background: '#1a3134' }} />
<div style={{ flex: 1, padding: 10 }}>
<div style={{ height: 8, background: '#fff', borderRadius: 4, marginBottom: 6 }} />
<div style={{ height: 8, width: '45%', background: '#004d43', borderRadius: 4 }} />
</div>
</div>
<div className="card-body" style={{ padding: 12 }}>
<div className="fw-600">Light</div><div className="lr-sub">Clean and bright</div>
</div>
</div>
<div className="card theme-opt" style={{ cursor: 'pointer', overflow: 'hidden' }} onClick={() => pick('dark')}>
<div style={{ height: 80, background: '#0e1d1f', borderBottom: '1px solid var(--border)', display: 'flex' }}>
<div style={{ width: '30%', background: '#0a1618' }} />
<div style={{ flex: 1, padding: 10 }}>
<div style={{ height: 8, background: '#24403f', borderRadius: 4, marginBottom: 6 }} />
<div style={{ height: 8, width: '45%', background: '#ceff71', borderRadius: 4 }} />
</div>
</div>
<div className="card-body" style={{ padding: 12 }}>
<div className="fw-600">Dark</div><div className="lr-sub">Easy on the eyes</div>
</div>
</div>
<div className="card theme-opt" style={{ cursor: 'pointer', overflow: 'hidden' }} onClick={() => pick('system')}>
<div style={{ height: 80, background: 'linear-gradient(90deg,#f1f7f4 50%,#0e1d1f 50%)', borderBottom: '1px solid var(--border)' }} />
<div className="card-body" style={{ padding: 12 }}>
<div className="fw-600">System</div><div className="lr-sub">Match OS setting</div>
</div>
</div>
</div>
</div>
</div>
)
}