WebOS completion
This commit is contained in:
@@ -14,11 +14,14 @@ import {
|
||||
Copy,
|
||||
Check,
|
||||
ChevronDown,
|
||||
LogOut,
|
||||
type LucideIcon,
|
||||
} from 'lucide-react';
|
||||
import { useStore } from '@/store/useStore';
|
||||
import { useCurrency, CURRENCY_OPTIONS } from '@/store/useCurrencyStore';
|
||||
import type { CurrencyCode } from '@/store/useCurrencyStore';
|
||||
import { API_URL } from '@/lib/api';
|
||||
import { VELOCITY_TOKEN_KEY, clearVelocityToken, getVelocityToken, normalizeVelocityRole } from '@/lib/velocityPlatformClient';
|
||||
|
||||
// ── Design tokens (matching inventory glassmorphism) ─────────────────────────
|
||||
const GLASS = {
|
||||
@@ -223,6 +226,7 @@ function DarkInput({ type = 'text', defaultValue, placeholder }: { type?: string
|
||||
// ── System Status ────────────────────────────────────────────────────────────
|
||||
function SystemStatusCard() {
|
||||
const { status, updateStatus } = useStore();
|
||||
const lastSync = status.lastSync instanceof Date ? status.lastSync : new Date(status.lastSync);
|
||||
|
||||
return (
|
||||
<GlassCard delay={0}>
|
||||
@@ -238,7 +242,7 @@ function SystemStatusCard() {
|
||||
<div>
|
||||
<p className="text-white text-sm font-medium">Backend Connection</p>
|
||||
<p className="text-xs mt-0.5" style={{ color: 'hsl(var(--muted-fg))' }}>
|
||||
{status.isConnected ? 'Connected to local server' : 'Connection lost'}
|
||||
{status.isConnected ? 'Connected to live Velocity backend' : 'Connection unavailable'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -257,7 +261,7 @@ function SystemStatusCard() {
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{[
|
||||
{ label: 'Version', value: status.version },
|
||||
{ label: 'Last Sync', value: status.lastSync.toLocaleTimeString() },
|
||||
{ label: 'Last Sync', value: Number.isNaN(lastSync.getTime()) ? 'Unavailable' : lastSync.toLocaleTimeString() },
|
||||
].map(({ label, value }) => (
|
||||
<div key={label} className="p-3 rounded-xl" style={INNER_SURFACE}>
|
||||
<p className="text-xs mb-1" style={{ color: 'hsl(var(--muted-fg))' }}>{label}</p>
|
||||
@@ -274,7 +278,10 @@ function SystemStatusCard() {
|
||||
style={INNER_SURFACE}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.97 }}
|
||||
onClick={() => updateStatus({ serverStatus: 'syncing' })}
|
||||
onClick={() => {
|
||||
updateStatus({ serverStatus: 'syncing' });
|
||||
window.location.reload();
|
||||
}}
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" style={{ color: 'hsl(var(--accent))' }} />
|
||||
<span className="text-white">Sync Now</span>
|
||||
@@ -285,6 +292,7 @@ function SystemStatusCard() {
|
||||
style={INNER_SURFACE}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.97 }}
|
||||
onClick={() => window.location.reload()}
|
||||
>
|
||||
<Power className="w-4 h-4" style={{ color: 'hsl(var(--muted-fg))' }} />
|
||||
<span className="text-white">Restart</span>
|
||||
@@ -296,17 +304,10 @@ function SystemStatusCard() {
|
||||
}
|
||||
|
||||
// ── iOS Device Connection ────────────────────────────────────────────────────
|
||||
function IOSConnectionCard() {
|
||||
const [paired, setPaired] = useState(false);
|
||||
const [pairing, setPairing] = useState(false);
|
||||
function CompanionSurfacesCard() {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const pairingCode = 'VLC-7F3A-9B2D';
|
||||
const deviceIp = '192.168.1.42:8765';
|
||||
|
||||
const handlePair = () => {
|
||||
setPairing(true);
|
||||
setTimeout(() => { setPairing(false); setPaired(true); }, 2000);
|
||||
};
|
||||
const token = getVelocityToken();
|
||||
const maskedToken = token ? `${token.slice(0, 8)}...${token.slice(-6)}` : 'No active bearer token';
|
||||
|
||||
const handleCopy = (text: string) => {
|
||||
void navigator.clipboard.writeText(text);
|
||||
@@ -316,85 +317,58 @@ function IOSConnectionCard() {
|
||||
|
||||
return (
|
||||
<GlassCard delay={0.05}>
|
||||
<SectionHeader icon={Smartphone} title="iOS App / Device" accent="#a78bfa" />
|
||||
<SectionHeader icon={Smartphone} title="Companion Surfaces" accent="#a78bfa" />
|
||||
<div className="px-6 pb-6 space-y-3">
|
||||
|
||||
{/* Status */}
|
||||
<div className="flex items-center justify-between p-4 rounded-xl" style={INNER_SURFACE}>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative w-3 h-3">
|
||||
<div className={`w-3 h-3 rounded-full ${paired ? 'bg-green-500' : 'bg-zinc-500'}`} />
|
||||
{paired && <div className="absolute inset-0 rounded-full bg-green-500 status-pulse" />}
|
||||
<div className={`w-3 h-3 rounded-full ${token ? 'bg-green-500' : 'bg-zinc-500'}`} />
|
||||
{token && <div className="absolute inset-0 rounded-full bg-green-500 status-pulse" />}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white text-sm font-medium">{paired ? 'iPhone Paired' : 'No Device Paired'}</p>
|
||||
<p className="text-white text-sm font-medium">WebOS Session Token</p>
|
||||
<p className="text-xs mt-0.5" style={{ color: 'hsl(var(--muted-fg))' }}>
|
||||
{paired ? "Ahmed’s iPhone 15 Pro" : 'Open Velocity iOS app to connect'}
|
||||
{token ? 'Reusable by Oracle and protected WebOS routes.' : 'No authenticated backend session is currently present.'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{paired
|
||||
? <span className="px-2.5 py-1 rounded-full text-[11px] font-medium" style={{ background: 'rgba(34,197,94,0.15)', color: '#86efac' }}>CONNECTED</span>
|
||||
{token
|
||||
? <span className="px-2.5 py-1 rounded-full text-[11px] font-medium" style={{ background: 'rgba(34,197,94,0.15)', color: '#86efac' }}>ACTIVE</span>
|
||||
: <Wifi className="w-4 h-4" style={{ color: 'hsl(var(--muted-fg))' }} />
|
||||
}
|
||||
</div>
|
||||
|
||||
{/* Pairing code */}
|
||||
<div className="p-4 rounded-xl space-y-3" style={INNER_SURFACE}>
|
||||
<p className="text-xs font-medium uppercase tracking-widest" style={{ color: 'hsl(var(--muted-fg))' }}>Pairing Code</p>
|
||||
<p className="text-xs font-medium uppercase tracking-widest" style={{ color: 'hsl(var(--muted-fg))' }}>Runtime Access</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-2xl font-bold tracking-[0.2em] text-white font-mono">{pairingCode}</p>
|
||||
<p className="text-sm font-bold tracking-[0.06em] text-white font-mono">{maskedToken}</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleCopy(pairingCode)}
|
||||
onClick={() => handleCopy(token ?? '')}
|
||||
disabled={!token}
|
||||
className="p-2 rounded-lg transition-colors"
|
||||
style={{ background: 'rgba(255,255,255,0.06)' }}
|
||||
>
|
||||
{copied ? <Check className="w-4 h-4 text-green-400" /> : <Copy className="w-4 h-4" style={{ color: 'hsl(var(--muted-fg))' }} />}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-xs" style={{ color: 'hsl(var(--muted-fg))' }}>Local IP: <span className="text-white font-mono">{deviceIp}</span></p>
|
||||
<button type="button" onClick={() => handleCopy(deviceIp)} className="p-1 rounded transition-colors hover:opacity-70">
|
||||
<Copy className="w-3 h-3" style={{ color: 'hsl(var(--muted-fg))' }} />
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs" style={{ color: 'hsl(var(--muted-fg))' }}>
|
||||
Mobile and tablet pairing is intentionally deferred until the next delivery phase. This WebOS pass does not simulate device pairing.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-3">
|
||||
<motion.button
|
||||
type="button"
|
||||
onClick={handlePair}
|
||||
disabled={pairing || paired}
|
||||
onClick={() => window.location.reload()}
|
||||
className="flex-1 py-2.5 rounded-xl text-sm font-semibold flex items-center justify-center gap-2 transition-all"
|
||||
style={paired
|
||||
? { background: 'rgba(34,197,94,0.15)', color: '#86efac', border: '1px solid rgba(34,197,94,0.2)' }
|
||||
: { background: 'hsl(var(--accent))', color: 'hsl(var(--accent-fg))' }
|
||||
}
|
||||
whileHover={!paired ? { scale: 1.02 } : {}}
|
||||
whileTap={!paired ? { scale: 0.97 } : {}}
|
||||
style={{ background: 'hsl(var(--accent))', color: 'hsl(var(--accent-fg))' }}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.97 }}
|
||||
>
|
||||
{pairing ? (
|
||||
<><RefreshCw className="w-4 h-4 animate-spin" /> Pairing…</>
|
||||
) : paired ? (
|
||||
<><Check className="w-4 h-4" /> Paired</>
|
||||
) : (
|
||||
<><Smartphone className="w-4 h-4" /> Pair Device</>
|
||||
)}
|
||||
<><RefreshCw className="w-4 h-4" /> Refresh WebOS</>
|
||||
</motion.button>
|
||||
{paired && (
|
||||
<GhostButton onClick={() => setPaired(false)} danger>Unpair</GhostButton>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Push notifications toggle */}
|
||||
<div className="flex items-center justify-between pt-1">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-white">Push Notifications</p>
|
||||
<p className="text-xs mt-0.5" style={{ color: 'hsl(var(--muted-fg))' }}>Send alerts to paired iPhone</p>
|
||||
</div>
|
||||
<Toggle enabled={paired} onChange={() => { }} />
|
||||
<GhostButton onClick={() => handleCopy(API_URL)}>Copy API URL</GhostButton>
|
||||
</div>
|
||||
</div>
|
||||
</GlassCard>
|
||||
@@ -404,7 +378,12 @@ function IOSConnectionCard() {
|
||||
// ── Profile ──────────────────────────────────────────────────────────────────
|
||||
function ProfileSettings() {
|
||||
const { user } = useStore();
|
||||
const initials = user?.name.split(' ').map((n) => n[0]).join('') ?? 'AA';
|
||||
const initials = user?.name.split(' ').map((n) => n[0]).join('') ?? 'AU';
|
||||
const roleLabel = normalizeVelocityRole(user?.role)
|
||||
.toLowerCase()
|
||||
.split('_')
|
||||
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
||||
.join(' ') || 'Authenticated User';
|
||||
|
||||
return (
|
||||
<GlassCard delay={0.1}>
|
||||
@@ -420,19 +399,19 @@ function ProfileSettings() {
|
||||
<div>
|
||||
<p className="text-white font-semibold">{user?.name}</p>
|
||||
<p className="text-xs mt-0.5" style={{ color: 'hsl(var(--muted-fg))' }}>
|
||||
{user?.role === 'sales_director' ? 'Sales Director' : 'Administrator'}
|
||||
{roleLabel}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-0 -mx-6">
|
||||
<SettingsRow label="Full Name" description="Your display name">
|
||||
<DarkInput defaultValue={user?.name} />
|
||||
<SettingsRow label="Authenticated Name" description="Resolved from the active Velocity session">
|
||||
<span className="text-sm text-white">{user?.name ?? 'Unavailable'}</span>
|
||||
</SettingsRow>
|
||||
<SettingsRow label="Email" description="Notification email">
|
||||
<DarkInput type="email" defaultValue="ahmed@velocity.re" />
|
||||
<SettingsRow label="User ID" description="Backend principal identifier">
|
||||
<span className="text-sm font-mono text-white">{user?.id ?? 'Unavailable'}</span>
|
||||
</SettingsRow>
|
||||
<SettingsRow label="Phone" description="Contact number">
|
||||
<DarkInput type="tel" defaultValue="+971 50 123 4567" />
|
||||
<SettingsRow label="Role" description="Normalized access role from JWT claims">
|
||||
<span className="text-sm text-white">{roleLabel}</span>
|
||||
</SettingsRow>
|
||||
</div>
|
||||
</div>
|
||||
@@ -467,25 +446,35 @@ function NotificationSettings() {
|
||||
|
||||
// ── Security ─────────────────────────────────────────────────────────────────
|
||||
function SecuritySettings() {
|
||||
const [twoFactor, setTwoFactor] = useState(true);
|
||||
const [biometric, setBiometric] = useState(true);
|
||||
const [timeout, setTimeout_] = useState('30');
|
||||
const { logout } = useStore();
|
||||
const token = getVelocityToken();
|
||||
|
||||
return (
|
||||
<GlassCard delay={0.2}>
|
||||
<SectionHeader icon={Shield} title="Security" accent="#f59e0b" />
|
||||
<div>
|
||||
<SettingsRow label="Two-Factor Authentication" description="Require OTP for login">
|
||||
<Toggle enabled={twoFactor} onChange={setTwoFactor} />
|
||||
<SettingsRow label="Bearer Token" description="Current authenticated WebOS session state">
|
||||
<span className={`text-sm ${token ? 'text-emerald-300' : 'text-red-300'}`}>
|
||||
{token ? 'Present' : 'Missing'}
|
||||
</span>
|
||||
</SettingsRow>
|
||||
<SettingsRow label="Biometric Login" description="Use FaceID for authentication">
|
||||
<Toggle enabled={biometric} onChange={setBiometric} />
|
||||
<SettingsRow label="Password Management" description="Handled by the backend identity service">
|
||||
<span className="text-sm text-zinc-400">Managed outside WebOS</span>
|
||||
</SettingsRow>
|
||||
<SettingsRow label="Change Password" description="Update your password">
|
||||
<GhostButton>Change</GhostButton>
|
||||
</SettingsRow>
|
||||
<SettingsRow label="API Keys" description="Manage API access">
|
||||
<GhostButton>Manage</GhostButton>
|
||||
<SettingsRow label="API Session Reset" description="Clears the local bearer token and returns to login">
|
||||
<GhostButton
|
||||
danger
|
||||
onClick={() => {
|
||||
clearVelocityToken();
|
||||
logout();
|
||||
}}
|
||||
>
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<LogOut className="w-4 h-4" />
|
||||
Sign Out
|
||||
</span>
|
||||
</GhostButton>
|
||||
</SettingsRow>
|
||||
<SettingsRow label="Session Timeout" description="Auto-logout after inactivity">
|
||||
<DarkSelect
|
||||
@@ -566,13 +555,45 @@ function DisplaySettings() {
|
||||
// ── Data & Privacy ───────────────────────────────────────────────────────────
|
||||
function DataSettings() {
|
||||
const [retention, setRetention] = useState('90');
|
||||
const { leads, messages, units, status } = useStore();
|
||||
|
||||
const exportSnapshot = () => {
|
||||
const blob = new Blob([
|
||||
JSON.stringify(
|
||||
{
|
||||
exported_at: new Date().toISOString(),
|
||||
status,
|
||||
lead_count: leads.length,
|
||||
message_threads: Object.keys(messages).length,
|
||||
inventory_count: units.length,
|
||||
leads,
|
||||
messages,
|
||||
units,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const anchor = document.createElement('a');
|
||||
anchor.href = url;
|
||||
anchor.download = `velocity-webos-export-${Date.now()}.json`;
|
||||
anchor.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const clearUiCache = () => {
|
||||
localStorage.removeItem('velocity-webos-storage');
|
||||
localStorage.removeItem('pv-currency');
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
return (
|
||||
<GlassCard delay={0.3}>
|
||||
<SectionHeader icon={Database} title="Data & Privacy" />
|
||||
<div>
|
||||
<SettingsRow label="Auto-Backup" description="Automatically backup data daily">
|
||||
<Toggle enabled={true} onChange={() => { }} />
|
||||
<SettingsRow label="Auto-Backup" description="Operational data is owned by backend systems, not browser local storage">
|
||||
<span className="text-sm text-zinc-400">Backend managed</span>
|
||||
</SettingsRow>
|
||||
<SettingsRow label="Data Retention" description="How long to keep visitor data">
|
||||
<DarkSelect
|
||||
@@ -587,10 +608,10 @@ function DataSettings() {
|
||||
/>
|
||||
</SettingsRow>
|
||||
<SettingsRow label="Export Data" description="Download all your data">
|
||||
<GhostButton>Export</GhostButton>
|
||||
<GhostButton onClick={exportSnapshot}>Export</GhostButton>
|
||||
</SettingsRow>
|
||||
<SettingsRow label="Clear Cache" description="Remove temporary files" danger>
|
||||
<GhostButton danger>Clear</GhostButton>
|
||||
<GhostButton danger onClick={clearUiCache}>Clear</GhostButton>
|
||||
</SettingsRow>
|
||||
</div>
|
||||
</GlassCard>
|
||||
@@ -599,6 +620,7 @@ function DataSettings() {
|
||||
|
||||
// ── About ────────────────────────────────────────────────────────────────────
|
||||
function AboutSection() {
|
||||
const token = getVelocityToken();
|
||||
return (
|
||||
<GlassCard delay={0.35}>
|
||||
<SectionHeader icon={Wifi} title="About" />
|
||||
@@ -614,14 +636,10 @@ function AboutSection() {
|
||||
<div className="flex items-center justify-center gap-2 text-xs mb-6" style={{ color: 'hsl(var(--subtle-fg))' }}>
|
||||
<span>Version 2.1.0</span>
|
||||
<span>•</span>
|
||||
<span>Build 2024.02.18</span>
|
||||
<span>{token ? 'Authenticated session active' : 'No active session'}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-center gap-4">
|
||||
{['Terms of Service', 'Privacy Policy', 'Documentation'].map((label) => (
|
||||
<button key={label} type="button" className="text-sm transition-colors hover:opacity-80" style={{ color: 'hsl(var(--accent))' }}>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
<div className="text-xs text-zinc-500 mb-4">
|
||||
Backend origin: <span className="font-mono text-zinc-300">{API_URL}</span>
|
||||
</div>
|
||||
</div>
|
||||
</GlassCard>
|
||||
@@ -635,7 +653,7 @@ export function Settings() {
|
||||
{/* Row 1: System + iOS */}
|
||||
<div className="grid grid-cols-2 gap-4 relative z-40">
|
||||
<SystemStatusCard />
|
||||
<IOSConnectionCard />
|
||||
<CompanionSurfacesCard />
|
||||
</div>
|
||||
|
||||
{/* Row 2: Profile + Notifications */}
|
||||
|
||||
Reference in New Issue
Block a user