refactor: account settings page audit fixes

- Replace 15+ hard-coded color utilities with semantic tokens (text-danger,
  bg-success-dim, bg-warning-dim, bg-info-dim, etc.)
- Fix progress bar track/fill color collision (bg-accent → bg-muted track)
- Fix plan badge contrast: bg-accent text-muted-foreground → bg-accent-dim
  text-accent-text (WCAG AA violation)
- Add aria-label to role-change select, remove member button, resend invite button
- Add p-1 padding to icon-only buttons for minimum touch targets
- Replace inline inviteError/inviteSuccess state with toast.success/error
- Add toast feedback to handleSaveName and handleRemoveMember (were silent)
- Fix transition-all → transition-colors on all 7 nav link cards
- Fix hover:border-border (no-op) → hover:border-border-hover on nav cards
- Consolidate 5 separate isAccountOwner nav card blocks under Team Settings
  section label for visual hierarchy
- Remove duplicate embedded BrandingSettings component (Branding nav card exists)
- Fix "tree categories" → "flow categories" (user-facing terminology)
- Fix Target Lists description (remove reference to hidden maintenance flows)
- Fix UsageStat label "Trees" → "Flows"
- Remove orphaned BrandingSettings import and unused user store selector
- Fix duplicate text-xs class in SSO Enterprise badge
- Remove internal (Task 11) comment noise

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-03-30 05:44:57 +00:00
parent 691963d432
commit af524ec99d

View File

@@ -2,7 +2,6 @@ import { useEffect, useState } from 'react'
import { Link } from 'react-router-dom'
import { Building2, Users, Mail, Crown, Loader2, AlertCircle, Check, X, Settings, FolderTree, Server, RefreshCw, MessageSquareText, UserCog, AlertTriangle, Clock, Plug, Palette, ShieldCheck } from 'lucide-react'
import { PageMeta } from '@/components/common/PageMeta'
import { BrandingSettings } from '@/components/settings/BrandingSettings'
import { accountsApi } from '@/api/accounts'
import type { Account, AccountMember, AccountInvite } from '@/types'
import { TransferOwnershipModal } from '@/components/account/TransferOwnershipModal'
@@ -22,7 +21,6 @@ export function AccountSettingsPage() {
const { isAccountOwner } = usePermissions()
const { plan, limits, usage } = useSubscription()
const { defaultExportFormat, setDefaultExportFormat } = useUserPreferencesStore()
const user = useAuthStore((s) => s.user)
const subscription = useAuthStore((s) => s.subscription)
const [account, setAccount] = useState<Account | null>(null)
@@ -45,8 +43,6 @@ export function AccountSettingsPage() {
const [inviteEmail, setInviteEmail] = useState('')
const [inviteRole, setInviteRole] = useState('engineer')
const [isInviting, setIsInviting] = useState(false)
const [inviteError, setInviteError] = useState<string | null>(null)
const [inviteSuccess, setInviteSuccess] = useState<string | null>(null)
useEffect(() => {
loadData()
@@ -86,7 +82,9 @@ export function AccountSettingsPage() {
const updated = await accountsApi.updateMyAccount({ name: editedName.trim() })
setAccount(updated)
setIsEditingName(false)
toast.success('Account name updated')
} catch (err) {
toast.error('Failed to update account name')
console.error('Failed to update account name:', err)
} finally {
setIsSavingName(false)
@@ -98,17 +96,14 @@ export function AccountSettingsPage() {
if (!inviteEmail.trim()) return
setIsInviting(true)
setInviteError(null)
setInviteSuccess(null)
try {
await accountsApi.createInvite({ email: inviteEmail.trim(), role: inviteRole })
setInviteSuccess(`Invitation sent to ${inviteEmail}`)
toast.success(`Invitation sent to ${inviteEmail}`)
setInviteEmail('')
// Refresh invites list
const invitesData = await accountsApi.getInvites()
setInvites(invitesData)
} catch (err) {
setInviteError('Failed to send invitation')
toast.error('Failed to send invitation')
console.error(err)
} finally {
setIsInviting(false)
@@ -135,7 +130,9 @@ export function AccountSettingsPage() {
try {
await accountsApi.removeMember(userId)
setMembers(members.filter((m) => m.id !== userId))
toast.success('Member removed')
} catch (err) {
toast.error('Failed to remove member')
console.error('Failed to remove member:', err)
}
}
@@ -150,7 +147,7 @@ export function AccountSettingsPage() {
if (error) {
return (
<div className="rounded-md border border-red-400/20 bg-red-400/10 p-4 text-red-400">
<div className="rounded-md border border-danger/20 bg-danger-dim p-4 text-danger">
<div className="flex items-center gap-2">
<AlertCircle className="h-5 w-5" />
{error}
@@ -230,7 +227,7 @@ export function AccountSettingsPage() {
{isAccountOwner && (
<button
onClick={() => setIsEditingName(true)}
className="text-xs text-foreground hover:underline"
className="rounded px-1.5 py-0.5 text-xs text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
>
Edit
</button>
@@ -261,9 +258,9 @@ export function AccountSettingsPage() {
<span
className={cn(
'inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-sm font-medium',
plan === 'free' && 'bg-accent text-muted-foreground',
plan === 'pro' && 'bg-accent text-foreground',
plan === 'team' && 'bg-accent text-foreground'
plan === 'free' && 'bg-muted text-muted-foreground',
plan === 'pro' && 'bg-accent-dim text-accent-text',
plan === 'team' && 'bg-accent-dim text-accent-text'
)}
>
<Crown className="h-3.5 w-3.5" />
@@ -273,11 +270,11 @@ export function AccountSettingsPage() {
<span
className={cn(
'inline-flex rounded-full px-2.5 py-0.5 text-xs font-medium',
sub.status === 'active' && 'bg-green-500/10 text-emerald-400',
sub.status === 'trialing' && 'bg-blue-500/10 text-blue-400',
sub.status === 'past_due' && 'bg-yellow-500/10 text-yellow-400',
sub.status === 'canceled' && 'bg-red-400/10 text-red-400',
sub.status === 'orphaned' && 'bg-accent text-muted-foreground'
sub.status === 'active' && 'bg-success-dim text-success',
sub.status === 'trialing' && 'bg-info-dim text-info',
sub.status === 'past_due' && 'bg-warning-dim text-warning',
sub.status === 'canceled' && 'bg-danger-dim text-danger',
sub.status === 'orphaned' && 'bg-muted text-muted-foreground'
)}
>
{sub.status.charAt(0).toUpperCase() + sub.status.slice(1).replace('_', ' ')}
@@ -295,7 +292,7 @@ export function AccountSettingsPage() {
{limits && usage && (
<div className="mt-4 grid gap-3 sm:grid-cols-3">
<UsageStat
label="Trees"
label="Flows"
current={usage.tree_count}
max={limits.max_trees}
/>
@@ -350,12 +347,13 @@ export function AccountSettingsPage() {
</div>
<div className="flex items-center gap-3">
{member.account_role === 'owner' ? (
<span className="rounded-full px-2.5 py-0.5 text-xs font-medium bg-accent text-foreground">
<span className="rounded-full px-2.5 py-0.5 text-xs font-medium bg-accent-dim text-accent-text">
owner
</span>
) : (
<select
value={member.account_role}
aria-label={`Role for ${member.name}`}
onChange={async (e) => {
try {
const updated = await accountsApi.updateMemberRole(member.id, e.target.value)
@@ -375,15 +373,15 @@ export function AccountSettingsPage() {
</select>
)}
{!member.is_active && (
<span className="rounded-full bg-red-400/10 px-2 py-0.5 text-xs text-red-400">
<span className="rounded-full bg-danger-dim px-2 py-0.5 text-xs text-danger">
Inactive
</span>
)}
{member.account_role !== 'owner' && (
<button
onClick={() => handleRemoveMember(member.id)}
className="text-muted-foreground hover:text-red-400"
title="Remove member"
className="p-1 text-muted-foreground hover:text-danger"
aria-label={`Remove ${member.name}`}
>
<X className="h-4 w-4" />
</button>
@@ -438,12 +436,6 @@ export function AccountSettingsPage() {
</Button>
</div>
{inviteError && (
<p className="text-sm text-red-400">{inviteError}</p>
)}
{inviteSuccess && (
<p className="text-sm text-emerald-400">{inviteSuccess}</p>
)}
</form>
{/* Pending Invites */}
@@ -467,14 +459,14 @@ export function AccountSettingsPage() {
</p>
</div>
<div className="flex items-center gap-2">
<span className="rounded-full bg-accent px-2.5 py-0.5 text-xs text-muted-foreground">
<span className="rounded-full bg-muted px-2.5 py-0.5 text-xs text-muted-foreground">
{invite.role}
</span>
<button
onClick={() => handleResendInvite(invite.id)}
disabled={resendingId === invite.id}
className="text-muted-foreground hover:text-foreground disabled:opacity-50"
title="Resend invite"
className="p-1 text-muted-foreground hover:text-foreground disabled:opacity-50"
aria-label={`Resend invite to ${invite.email}`}
>
{resendingId === invite.id ? (
<Loader2 className="h-4 w-4 animate-spin" />
@@ -494,7 +486,7 @@ export function AccountSettingsPage() {
{/* Profile Settings Link */}
<Link
to="/account/profile"
className="bg-card border border-border rounded-xl p-4 sm:p-6 flex items-center justify-between group hover:border-border transition-all"
className="bg-card border border-border rounded-xl p-4 sm:p-6 flex items-center justify-between group hover:border-border-hover transition-colors"
>
<div className="flex items-center gap-3">
<UserCog className="h-5 w-5 text-muted-foreground" />
@@ -506,95 +498,89 @@ export function AccountSettingsPage() {
<span className="text-muted-foreground group-hover:text-foreground transition-colors">&rarr;</span>
</Link>
{/* Team Categories Link (owners only) */}
{/* Team Settings section (owners only) */}
{isAccountOwner && (
<Link
to="/account/categories"
className="bg-card border border-border rounded-xl p-4 sm:p-6 flex items-center justify-between group hover:border-border transition-all"
>
<div className="flex items-center gap-3">
<FolderTree className="h-5 w-5 text-muted-foreground" />
<div>
<h2 className="text-lg font-semibold text-foreground">Team Categories</h2>
<p className="text-sm text-muted-foreground">Manage tree categories for your team</p>
</div>
</div>
<span className="text-muted-foreground group-hover:text-foreground transition-colors">&rarr;</span>
</Link>
)}
<>
<p className="pt-2 text-xs font-medium uppercase tracking-wider text-muted-foreground">
Team Settings
</p>
{/* Target Lists Link (owners only) */}
{isAccountOwner && (
<Link
to="/account/target-lists"
className="bg-card border border-border rounded-xl p-4 sm:p-6 flex items-center justify-between group hover:border-border transition-all"
>
<div className="flex items-center gap-3">
<Server className="h-5 w-5 text-muted-foreground" />
<div>
<h2 className="text-lg font-semibold text-foreground">Target Lists</h2>
<p className="text-sm text-muted-foreground">Saved server lists for maintenance flow batch launching</p>
<Link
to="/account/categories"
className="bg-card border border-border rounded-xl p-4 sm:p-6 flex items-center justify-between group hover:border-border-hover transition-colors"
>
<div className="flex items-center gap-3">
<FolderTree className="h-5 w-5 text-muted-foreground" />
<div>
<h2 className="text-lg font-semibold text-foreground">Team Categories</h2>
<p className="text-sm text-muted-foreground">Manage flow categories for your team</p>
</div>
</div>
</div>
<span className="text-muted-foreground group-hover:text-foreground transition-colors">&rarr;</span>
</Link>
)}
<span className="text-muted-foreground group-hover:text-foreground transition-colors">&rarr;</span>
</Link>
{/* Chat Retention Link (owners only) */}
{isAccountOwner && (
<Link
to="/account/chat-retention"
className="bg-card border border-border rounded-xl p-4 sm:p-6 flex items-center justify-between group hover:border-border transition-all"
>
<div className="flex items-center gap-3">
<Clock className="h-5 w-5 text-muted-foreground" />
<div>
<h2 className="text-lg font-semibold text-foreground">Chat Retention</h2>
<p className="text-sm text-muted-foreground">Configure AI assistant conversation retention policies</p>
<Link
to="/account/target-lists"
className="bg-card border border-border rounded-xl p-4 sm:p-6 flex items-center justify-between group hover:border-border-hover transition-colors"
>
<div className="flex items-center gap-3">
<Server className="h-5 w-5 text-muted-foreground" />
<div>
<h2 className="text-lg font-semibold text-foreground">Target Lists</h2>
<p className="text-sm text-muted-foreground">Saved server and device lists for your team</p>
</div>
</div>
</div>
<span className="text-muted-foreground group-hover:text-foreground transition-colors">&rarr;</span>
</Link>
)}
<span className="text-muted-foreground group-hover:text-foreground transition-colors">&rarr;</span>
</Link>
{/* Integrations Link (owners only) */}
{isAccountOwner && (
<Link
to="/account/integrations"
className="bg-card border border-border rounded-xl p-4 sm:p-6 flex items-center justify-between group hover:border-border transition-all"
>
<div className="flex items-center gap-3">
<Plug className="h-5 w-5 text-muted-foreground" />
<div>
<h2 className="text-lg font-semibold text-foreground">Integrations</h2>
<p className="text-sm text-muted-foreground">Connect your PSA to sync session documentation to tickets</p>
<Link
to="/account/chat-retention"
className="bg-card border border-border rounded-xl p-4 sm:p-6 flex items-center justify-between group hover:border-border-hover transition-colors"
>
<div className="flex items-center gap-3">
<Clock className="h-5 w-5 text-muted-foreground" />
<div>
<h2 className="text-lg font-semibold text-foreground">Chat Retention</h2>
<p className="text-sm text-muted-foreground">Configure AI assistant conversation retention policies</p>
</div>
</div>
</div>
<span className="text-muted-foreground group-hover:text-foreground transition-colors">&rarr;</span>
</Link>
)}
<span className="text-muted-foreground group-hover:text-foreground transition-colors">&rarr;</span>
</Link>
{/* Branding Link (owners only) */}
{isAccountOwner && (
<Link
to="/account/branding"
className="bg-card border border-border rounded-xl p-4 sm:p-6 flex items-center justify-between group hover:border-border transition-all"
>
<div className="flex items-center gap-3">
<Palette className="h-5 w-5 text-muted-foreground" />
<div>
<h2 className="text-lg font-semibold text-foreground">Branding</h2>
<p className="text-sm text-muted-foreground">Customize logo, accent color, and company name</p>
<Link
to="/account/integrations"
className="bg-card border border-border rounded-xl p-4 sm:p-6 flex items-center justify-between group hover:border-border-hover transition-colors"
>
<div className="flex items-center gap-3">
<Plug className="h-5 w-5 text-muted-foreground" />
<div>
<h2 className="text-lg font-semibold text-foreground">Integrations</h2>
<p className="text-sm text-muted-foreground">Connect your PSA to sync session documentation to tickets</p>
</div>
</div>
</div>
<span className="text-muted-foreground group-hover:text-foreground transition-colors">&rarr;</span>
</Link>
<span className="text-muted-foreground group-hover:text-foreground transition-colors">&rarr;</span>
</Link>
<Link
to="/account/branding"
className="bg-card border border-border rounded-xl p-4 sm:p-6 flex items-center justify-between group hover:border-border-hover transition-colors"
>
<div className="flex items-center gap-3">
<Palette className="h-5 w-5 text-muted-foreground" />
<div>
<h2 className="text-lg font-semibold text-foreground">Branding</h2>
<p className="text-sm text-muted-foreground">Customize logo, accent color, and company name</p>
</div>
</div>
<span className="text-muted-foreground group-hover:text-foreground transition-colors">&rarr;</span>
</Link>
</>
)}
{/* Feedback Link (all users) */}
<Link
to="/feedback"
className="bg-card border border-border rounded-xl p-4 sm:p-6 flex items-center justify-between group hover:border-border transition-all"
className="bg-card border border-border rounded-xl p-4 sm:p-6 flex items-center justify-between group hover:border-border-hover transition-colors"
>
<div className="flex items-center gap-3">
<MessageSquareText className="h-5 w-5 text-muted-foreground" />
@@ -606,11 +592,6 @@ export function AccountSettingsPage() {
<span className="text-muted-foreground group-hover:text-foreground transition-colors">&rarr;</span>
</Link>
{/* Branding Section (owners only) */}
{isAccountOwner && user?.team_id && (
<BrandingSettings teamId={user.team_id} />
)}
{/* Preferences Section */}
<div className="bg-card border border-border rounded-xl p-4 sm:p-6">
<div className="flex items-center gap-2">
@@ -647,13 +628,13 @@ export function AccountSettingsPage() {
</select>
</div>
</div>
{/* SSO Section (Task 11) */}
{/* SSO Section */}
{isAccountOwner && (
<div className="bg-card border border-border rounded-xl p-4 sm:p-6">
<div className="flex items-center gap-2 mb-3">
<ShieldCheck className="h-5 w-5 text-muted-foreground" />
<h2 className="text-lg font-semibold text-foreground">Single Sign-On (SSO)</h2>
<span className="inline-flex items-center rounded-full bg-accent-dim px-2.5 py-0.5 text-xs font-sans text-xs font-medium text-primary">
<span className="inline-flex items-center rounded-full bg-accent-dim px-2.5 py-0.5 text-xs font-medium text-primary">
Enterprise
</span>
</div>
@@ -665,8 +646,8 @@ export function AccountSettingsPage() {
href="mailto:support@resolutionflow.com?subject=SSO%20Setup%20Request"
className={cn(
'inline-flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium',
'bg-[rgba(255,255,255,0.04)] border border-[rgba(255,255,255,0.06)] text-foreground',
'hover:border-[rgba(255,255,255,0.12)] transition-all'
'bg-muted border border-border text-foreground',
'hover:border-border-hover transition-colors'
)}
>
Contact Us
@@ -675,9 +656,9 @@ export function AccountSettingsPage() {
)}
{/* Danger Zone */}
<div className="rounded-xl border border-rose-500/20 p-4 sm:p-6">
<div className="rounded-xl border border-danger/20 p-4 sm:p-6">
<div className="flex items-center gap-2 mb-4">
<AlertTriangle className="h-5 w-5 text-rose-500" />
<AlertTriangle className="h-5 w-5 text-danger" />
<h2 className="text-lg font-semibold text-foreground">Danger Zone</h2>
</div>
@@ -693,7 +674,7 @@ export function AccountSettingsPage() {
variant="secondary"
size="sm"
onClick={() => setShowTransferModal(true)}
className="border-amber-500/30 text-amber-400 hover:bg-amber-500/10"
className="border-warning/30 text-warning hover:bg-warning-dim"
>
Transfer
</Button>
@@ -774,7 +755,7 @@ function UsageStat({
<p
className={cn(
'mt-1 text-lg font-semibold',
isAtLimit ? 'text-red-400' : isNearLimit ? 'text-yellow-400' : 'text-foreground'
isAtLimit ? 'text-danger' : isNearLimit ? 'text-warning' : 'text-foreground'
)}
>
{current}
@@ -783,11 +764,11 @@ function UsageStat({
</span>
</p>
{!isUnlimited && (
<div className="mt-2 h-1.5 overflow-hidden rounded-full bg-accent">
<div className="mt-2 h-1.5 overflow-hidden rounded-full bg-muted">
<div
className={cn(
'h-full rounded-full transition-all',
isAtLimit ? 'bg-red-400' : isNearLimit ? 'bg-yellow-500' : 'bg-primary'
isAtLimit ? 'bg-danger' : isNearLimit ? 'bg-warning' : 'bg-primary'
)}
style={{ width: `${percentage}%` }}
/>