mirror of
http://88.130.71.182:3000/BlitTech/contexta_fe.git
synced 2026-06-12 23:23:22 +00:00
Updates Mar6
This commit is contained in:
@@ -1,35 +1,20 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import React, { useState, useCallback } from 'react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useNavigate, useLocation, Link } from 'react-router-dom'
|
||||
import { billingAPI } from '@/services/api'
|
||||
import { billingAPI, authAPI } from '@/services/api'
|
||||
import { useAuthStore } from '@/store/authStore'
|
||||
import { Button, Card, Input, Badge } from '@/components/ui'
|
||||
import { Button, Card, Input } from '@/components/ui'
|
||||
import { getPlanColor, formatDate } from '@/lib/utils'
|
||||
import { CreditCard, User, Shield, Download, ExternalLink } from 'lucide-react'
|
||||
import { CreditCard, User, ExternalLink, AlertTriangle } from 'lucide-react'
|
||||
|
||||
export const SettingsPage: React.FC = () => {
|
||||
// BUG-04 FIX: Removed unused 'updateUser' from destructuring
|
||||
const { user } = useAuthStore()
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
|
||||
// BUG-06 FIX: Sync tab with URL path on mount and when path changes
|
||||
const getTabFromPath = (pathname: string): 'profile' | 'billing' => {
|
||||
if (pathname === '/settings/billing') return 'billing'
|
||||
return 'profile'
|
||||
}
|
||||
|
||||
const [tab, setTab] = useState<'profile' | 'billing'>(getTabFromPath(location.pathname))
|
||||
const [toast, setToast] = useState('')
|
||||
|
||||
// Keep tab in sync if URL changes externally
|
||||
useEffect(() => {
|
||||
setTab(getTabFromPath(location.pathname))
|
||||
}, [location.pathname])
|
||||
const tab: 'profile' | 'billing' = location.pathname === '/settings/billing' ? 'billing' : 'profile'
|
||||
|
||||
// Update URL when tab changes
|
||||
const handleTabChange = useCallback((newTab: 'profile' | 'billing') => {
|
||||
setTab(newTab)
|
||||
const newPath = newTab === 'billing' ? '/settings/billing' : '/settings'
|
||||
if (location.pathname !== newPath) {
|
||||
navigate(newPath, { replace: true })
|
||||
@@ -38,7 +23,7 @@ export const SettingsPage: React.FC = () => {
|
||||
|
||||
const showToast = (msg: string) => {
|
||||
setToast(msg)
|
||||
setTimeout(() => setToast(''), 3000)
|
||||
setTimeout(() => setToast(''), 3500)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -80,30 +65,153 @@ export const SettingsPage: React.FC = () => {
|
||||
}
|
||||
|
||||
const ProfileSettings: React.FC<{ onToast: (msg: string) => void }> = ({ onToast }) => {
|
||||
const { user } = useAuthStore()
|
||||
const { user, setAuth, token, logout } = useAuthStore()
|
||||
const navigate = useNavigate()
|
||||
const [companyName, setCompanyName] = useState(user?.company_name || '')
|
||||
const [currentPassword, setCurrentPassword] = useState('')
|
||||
const [newPassword, setNewPassword] = useState('')
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
||||
const [deleteConfirm, setDeleteConfirm] = useState('')
|
||||
const [deleting, setDeleting] = useState(false)
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true)
|
||||
try {
|
||||
const payload: { company_name?: string; current_password?: string; new_password?: string } = {}
|
||||
if (companyName !== user?.company_name) payload.company_name = companyName
|
||||
if (newPassword) {
|
||||
payload.current_password = currentPassword
|
||||
payload.new_password = newPassword
|
||||
}
|
||||
if (Object.keys(payload).length === 0) {
|
||||
onToast('No changes to save')
|
||||
return
|
||||
}
|
||||
const updated = await authAPI.updateProfile(payload)
|
||||
setAuth(updated, token || '')
|
||||
setCurrentPassword('')
|
||||
setNewPassword('')
|
||||
onToast('Profile updated successfully')
|
||||
} catch (err) {
|
||||
const e = err as { response?: { data?: { detail?: string } } }
|
||||
onToast(e.response?.data?.detail || 'Failed to update profile')
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteAccount = async () => {
|
||||
if (deleteConfirm !== 'DELETE') return
|
||||
setDeleting(true)
|
||||
try {
|
||||
await authAPI.deleteAccount()
|
||||
logout()
|
||||
navigate('/')
|
||||
} catch (err) {
|
||||
const e = err as { response?: { data?: { detail?: string } } }
|
||||
onToast(e.response?.data?.detail || 'Failed to delete account')
|
||||
setDeleting(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="p-6 space-y-4">
|
||||
<h2 className="font-semibold text-gray-900">Profile Information</h2>
|
||||
<Input label="Email" value={user?.email || ''} disabled />
|
||||
<Input label="Company Name" value={user?.company_name || ''} disabled hint="Contact support to change company name" />
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1.5">Plan</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`px-3 py-1 text-sm font-medium rounded-full capitalize ${getPlanColor(user?.plan || 'free')}`}>
|
||||
{user?.plan || 'free'}
|
||||
</span>
|
||||
<Link to="/pricing" className="text-sm text-primary-600 hover:underline">
|
||||
Manage plan
|
||||
</Link>
|
||||
<div className="space-y-4">
|
||||
<Card className="p-6 space-y-4">
|
||||
<h2 className="font-semibold text-gray-900">Profile Information</h2>
|
||||
<Input label="Email" value={user?.email || ''} disabled hint="Email cannot be changed" />
|
||||
<Input
|
||||
label="Company Name"
|
||||
value={companyName}
|
||||
onChange={e => setCompanyName(e.target.value)}
|
||||
placeholder="Your company name"
|
||||
/>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1.5">Plan</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`px-3 py-1 text-sm font-medium rounded-full capitalize ${getPlanColor(user?.plan || 'free')}`}>
|
||||
{user?.plan || 'free'}
|
||||
</span>
|
||||
<Link to="/pricing" className="text-sm text-primary-600 hover:underline">
|
||||
Manage plan
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6 space-y-4">
|
||||
<h2 className="font-semibold text-gray-900">Change Password</h2>
|
||||
<Input
|
||||
label="Current Password"
|
||||
type="password"
|
||||
value={currentPassword}
|
||||
onChange={e => setCurrentPassword(e.target.value)}
|
||||
placeholder="Enter current password"
|
||||
/>
|
||||
<Input
|
||||
label="New Password"
|
||||
type="password"
|
||||
value={newPassword}
|
||||
onChange={e => setNewPassword(e.target.value)}
|
||||
placeholder="Min 8 characters"
|
||||
hint="Leave blank to keep current password"
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Button onClick={handleSave} loading={saving}>
|
||||
Save Changes
|
||||
</Button>
|
||||
|
||||
{/* Danger Zone */}
|
||||
<Card className="p-6 border-red-200 bg-red-50/30">
|
||||
<h2 className="font-semibold text-red-800 mb-2 flex items-center gap-1.5">
|
||||
<AlertTriangle className="w-4 h-4" />
|
||||
Danger Zone
|
||||
</h2>
|
||||
<p className="text-sm text-red-700 mb-4">
|
||||
Permanently delete your account, all chatbots, documents, and data. This cannot be undone.
|
||||
</p>
|
||||
<Button variant="outline" className="border-red-300 text-red-700 hover:bg-red-50" onClick={() => setShowDeleteModal(true)}>
|
||||
Delete Account
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
{/* Delete Account Modal */}
|
||||
{showDeleteModal && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-2xl p-6 w-full max-w-md shadow-xl">
|
||||
<h3 className="text-lg font-bold text-gray-900 mb-2">Delete Account</h3>
|
||||
<p className="text-sm text-gray-500 mb-4">
|
||||
This will permanently delete your account and all associated data including chatbots, documents, conversations, and leads.
|
||||
<strong className="text-red-600"> This action cannot be undone.</strong>
|
||||
</p>
|
||||
<p className="text-sm text-gray-700 mb-2">Type <strong>DELETE</strong> to confirm:</p>
|
||||
<Input
|
||||
value={deleteConfirm}
|
||||
onChange={e => setDeleteConfirm(e.target.value)}
|
||||
placeholder="DELETE"
|
||||
/>
|
||||
<div className="flex gap-3 mt-4">
|
||||
<Button variant="outline" className="flex-1" onClick={() => { setShowDeleteModal(false); setDeleteConfirm('') }}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
className="flex-1 bg-red-600 hover:bg-red-700"
|
||||
disabled={deleteConfirm !== 'DELETE'}
|
||||
loading={deleting}
|
||||
onClick={handleDeleteAccount}
|
||||
>
|
||||
Delete Account
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const BillingSettings: React.FC<{ onToast: (msg: string) => void }> = ({ onToast }) => {
|
||||
const { user } = useAuthStore()
|
||||
const navigate = useNavigate()
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
@@ -117,14 +225,25 @@ const BillingSettings: React.FC<{ onToast: (msg: string) => void }> = ({ onToast
|
||||
try {
|
||||
const { url } = await billingAPI.createPortal(window.location.href)
|
||||
window.location.href = url
|
||||
} catch (err: any) {
|
||||
onToast(err.response?.data?.detail || 'Failed to open billing portal')
|
||||
} catch (err) {
|
||||
const e = err as { response?: { data?: { detail?: string } } }
|
||||
onToast(e.response?.data?.detail || 'Failed to open billing portal')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const isPaid = subscription?.plan && subscription.plan !== 'free'
|
||||
const plan = subscription?.plan || 'free'
|
||||
const isPaid = plan !== 'free'
|
||||
|
||||
const planFeatures: Record<string, { published: string; conversations: string; codeExport: string }> = {
|
||||
free: { published: '1', conversations: '100/month', codeExport: '❌ Agency+ only' },
|
||||
starter: { published: '1', conversations: '1,500/month', codeExport: '❌ Agency+ only' },
|
||||
business: { published: '3', conversations: '5,000/month', codeExport: '❌ Agency+ only' },
|
||||
agency: { published: 'Unlimited', conversations: '20,000/month', codeExport: '✅ Included' },
|
||||
enterprise: { published: 'Unlimited', conversations: 'Unlimited', codeExport: '✅ Included' },
|
||||
}
|
||||
const features = planFeatures[plan] || planFeatures.free
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
@@ -132,13 +251,14 @@ const BillingSettings: React.FC<{ onToast: (msg: string) => void }> = ({ onToast
|
||||
<h2 className="font-semibold text-gray-900 mb-4">Current Plan</h2>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<span className={`px-3 py-1 text-sm font-semibold rounded-full capitalize ${getPlanColor(subscription?.plan || 'free')}`}>
|
||||
{subscription?.plan || 'free'}
|
||||
</span>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Status: <span className={subscription?.status === 'active' ? 'text-green-600' : 'text-red-600'}>
|
||||
{subscription?.status || 'active'}
|
||||
<span className={`px-3 py-1 text-sm font-semibold rounded-full capitalize ${getPlanColor(plan)}`}>
|
||||
{plan}
|
||||
</span>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Status:{' '}
|
||||
<span className={subscription?.status === 'active' ? 'text-green-600' : 'text-red-600'}>
|
||||
{subscription?.status || 'active'}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
{isPaid && subscription?.current_period_end && (
|
||||
@@ -168,10 +288,9 @@ const BillingSettings: React.FC<{ onToast: (msg: string) => void }> = ({ onToast
|
||||
<h3 className="font-semibold text-gray-900 mb-4">Plan Features</h3>
|
||||
<div className="space-y-3">
|
||||
{[
|
||||
{ label: 'Chatbots created', value: 'Unlimited' },
|
||||
{ label: 'Chatbots published', value: subscription?.plan === 'pro' ? '3' : subscription?.plan === 'starter' ? '1' : '0 (upgrade to publish)' },
|
||||
{ label: 'Conversations/month', value: subscription?.plan === 'pro' ? '20,000' : subscription?.plan === 'starter' ? '5,000' : 'Unlimited (preview)' },
|
||||
{ label: 'Code export', value: subscription?.plan === 'pro' || subscription?.plan === 'enterprise' ? '✅ Included' : '❌ Pro+ only' },
|
||||
{ label: 'Chatbots published', value: features.published },
|
||||
{ label: 'Conversations/month', value: features.conversations },
|
||||
{ label: 'Code export', value: features.codeExport },
|
||||
].map(({ label, value }) => (
|
||||
<div key={label} className="flex justify-between py-2 border-b border-gray-100 last:border-0">
|
||||
<span className="text-sm text-gray-600">{label}</span>
|
||||
@@ -182,4 +301,4 @@ const BillingSettings: React.FC<{ onToast: (msg: string) => void }> = ({ onToast
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user