Files
Project_Velocity/app/src/components/modules/Settings.tsx
2026-04-12 02:01:36 +05:30

661 lines
26 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
User,
Bell,
Shield,
Database,
Monitor,
RefreshCw,
Power,
Server,
Smartphone,
Wifi,
Copy,
Check,
ChevronDown,
type LucideIcon,
} from 'lucide-react';
import { useStore } from '@/store/useStore';
import { useCurrency, CURRENCY_OPTIONS } from '@/store/useCurrencyStore';
import type { CurrencyCode } from '@/store/useCurrencyStore';
// ── Design tokens (matching inventory glassmorphism) ─────────────────────────
const GLASS = {
background: 'rgba(14, 16, 21, 0.72)',
border: '1px solid rgba(255,255,255,0.08)',
backdropFilter: 'blur(18px)',
WebkitBackdropFilter: 'blur(18px)',
} as const;
const INNER_SURFACE = {
background: 'rgba(255,255,255,0.04)',
border: '1px solid rgba(255,255,255,0.07)',
} as const;
// ── Shared primitives ────────────────────────────────────────────────────────
function GlassCard({
children,
delay = 0,
className = '',
}: {
children: React.ReactNode;
delay?: number;
className?: string;
}) {
return (
<motion.div
className={`relative rounded-2xl ${className}`}
style={GLASS}
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.35, delay }}
>
{children}
</motion.div>
);
}
function SectionHeader({ icon: Icon, title, accent = 'hsl(var(--accent))' }: { icon: LucideIcon; title: string; accent?: string }) {
return (
<div className="flex items-center gap-3 px-6 pt-6 pb-4 mb-2" style={{ borderBottom: '1px solid rgba(255,255,255,0.06)' }}>
<div
className="w-9 h-9 rounded-xl flex items-center justify-center flex-shrink-0"
style={{ background: `${accent}22`, border: `1px solid ${accent}33` }}
>
<Icon className="w-4 h-4" style={{ color: accent }} />
</div>
<h3 className="text-white font-semibold text-base tracking-tight">{title}</h3>
</div>
);
}
function SettingsRow({
label,
description,
children,
danger = false,
}: {
label: string;
description?: string;
children: React.ReactNode;
danger?: boolean;
}) {
return (
<div
className="flex items-center justify-between px-6 py-3.5"
style={{ borderBottom: '1px solid rgba(255,255,255,0.04)' }}
>
<div className="flex-1 min-w-0 pr-4">
<p className={`text-sm font-medium ${danger ? 'text-red-400' : 'text-white'}`}>{label}</p>
{description && (
<p className="text-xs mt-0.5" style={{ color: 'hsl(var(--muted-fg))' }}>{description}</p>
)}
</div>
<div className="flex-shrink-0">{children}</div>
</div>
);
}
// ── Toggle ───────────────────────────────────────────────────────────────────
function Toggle({ enabled, onChange }: { enabled: boolean; onChange: (v: boolean) => void }) {
return (
<button
type="button"
onClick={() => onChange(!enabled)}
className="relative w-11 h-6 rounded-full transition-colors duration-200 flex-shrink-0"
style={{ background: enabled ? 'hsl(var(--accent))' : 'rgba(255,255,255,0.12)' }}
>
<motion.div
className="absolute top-1 w-4 h-4 rounded-full bg-white shadow-sm"
animate={{ left: enabled ? '24px' : '4px' }}
transition={{ type: 'spring', stiffness: 500, damping: 32 }}
/>
</button>
);
}
// ── Dark Select ──────────────────────────────────────────────────────────────
function DarkSelect({
value,
onChange,
options,
}: {
value: string;
onChange: (v: string) => void;
options: { value: string; label: string }[];
}) {
const [open, setOpen] = useState(false);
const current = options.find((o) => o.value === value) ?? options[0];
return (
<div className="relative">
<button
type="button"
onClick={() => setOpen((p) => !p)}
className="flex items-center gap-2 rounded-xl px-3 py-2 text-sm text-white transition-colors min-w-[140px] justify-between"
style={INNER_SURFACE}
>
<span>{current.label}</span>
<ChevronDown
className="w-3.5 h-3.5 transition-transform flex-shrink-0"
style={{ color: 'hsl(var(--muted-fg))', transform: open ? 'rotate(180deg)' : 'rotate(0deg)' }}
/>
</button>
<AnimatePresence>
{open && (
<motion.div
className="absolute right-0 top-full mt-1.5 z-50 rounded-xl overflow-hidden min-w-[160px]"
style={{
background: 'rgba(18,20,26,0.96)',
border: '1px solid rgba(255,255,255,0.1)',
backdropFilter: 'blur(24px)',
WebkitBackdropFilter: 'blur(24px)',
boxShadow: '0 16px 48px rgba(0,0,0,0.5)',
}}
initial={{ opacity: 0, y: -6, scale: 0.97 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -4, scale: 0.97 }}
transition={{ duration: 0.15 }}
>
{options.map((opt) => (
<button
key={opt.value}
type="button"
onClick={() => { onChange(opt.value); setOpen(false); }}
className="w-full text-left px-4 py-2.5 text-sm transition-colors"
style={{
color: opt.value === value ? 'hsl(var(--accent))' : 'rgba(255,255,255,0.85)',
background: opt.value === value ? 'hsl(var(--accent) / 0.1)' : 'transparent',
}}
onMouseEnter={(e) => { if (opt.value !== value) (e.currentTarget as HTMLElement).style.background = 'rgba(255,255,255,0.06)'; }}
onMouseLeave={(e) => { if (opt.value !== value) (e.currentTarget as HTMLElement).style.background = 'transparent'; }}
>
{opt.label}
</button>
))}
</motion.div>
)}
</AnimatePresence>
{open && <div className="fixed inset-0 z-40" onClick={() => setOpen(false)} />}
</div>
);
}
// ── Ghost button ─────────────────────────────────────────────────────────────
function GhostButton({ children, onClick, danger = false }: { children: React.ReactNode; onClick?: () => void; danger?: boolean }) {
return (
<motion.button
type="button"
onClick={onClick}
className="px-4 py-2 rounded-xl text-sm font-medium transition-colors"
style={danger
? { background: 'rgba(239,68,68,0.12)', color: '#f87171', border: '1px solid rgba(239,68,68,0.2)' }
: { ...INNER_SURFACE, color: 'rgba(255,255,255,0.8)' }
}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.97 }}
>
{children}
</motion.button>
);
}
// ── Text input ───────────────────────────────────────────────────────────────
function DarkInput({ type = 'text', defaultValue, placeholder }: { type?: string; defaultValue?: string; placeholder?: string }) {
return (
<input
type={type}
defaultValue={defaultValue}
placeholder={placeholder}
className="rounded-xl px-3 py-2 text-sm text-white w-48 focus:outline-none transition-all"
style={{
...INNER_SURFACE,
caretColor: 'hsl(var(--accent))',
}}
onFocus={(e) => { e.currentTarget.style.border = '1px solid hsl(var(--accent) / 0.4)'; }}
onBlur={(e) => { e.currentTarget.style.border = '1px solid rgba(255,255,255,0.07)'; }}
/>
);
}
// ── System Status ────────────────────────────────────────────────────────────
function SystemStatusCard() {
const { status, updateStatus } = useStore();
return (
<GlassCard delay={0}>
<SectionHeader icon={Server} title="System Status" />
<div className="px-6 pb-6 space-y-3">
{/* Connection pill */}
<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 ${status.isConnected ? 'bg-green-500' : 'bg-red-500'}`} />
{status.isConnected && <div className="absolute inset-0 rounded-full bg-green-500 status-pulse" />}
</div>
<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'}
</p>
</div>
</div>
<span
className="px-2.5 py-1 rounded-full text-[11px] font-medium"
style={status.isConnected
? { background: 'rgba(34,197,94,0.15)', color: '#86efac' }
: { background: 'rgba(239,68,68,0.15)', color: '#fca5a5' }
}
>
{status.serverStatus.toUpperCase()}
</span>
</div>
{/* Version / sync grid */}
<div className="grid grid-cols-2 gap-3">
{[
{ label: 'Version', value: status.version },
{ label: 'Last Sync', value: status.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>
<p className="text-white text-sm font-medium">{value}</p>
</div>
))}
</div>
{/* Actions */}
<div className="flex gap-3">
<motion.button
type="button"
className="flex-1 py-2.5 rounded-xl text-sm font-medium flex items-center justify-center gap-2 transition-colors"
style={INNER_SURFACE}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.97 }}
onClick={() => updateStatus({ serverStatus: 'syncing' })}
>
<RefreshCw className="w-4 h-4" style={{ color: 'hsl(var(--accent))' }} />
<span className="text-white">Sync Now</span>
</motion.button>
<motion.button
type="button"
className="flex-1 py-2.5 rounded-xl text-sm font-medium flex items-center justify-center gap-2 transition-colors"
style={INNER_SURFACE}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.97 }}
>
<Power className="w-4 h-4" style={{ color: 'hsl(var(--muted-fg))' }} />
<span className="text-white">Restart</span>
</motion.button>
</div>
</div>
</GlassCard>
);
}
// ── iOS Device Connection ────────────────────────────────────────────────────
function IOSConnectionCard() {
const [paired, setPaired] = useState(false);
const [pairing, setPairing] = useState(false);
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 handleCopy = (text: string) => {
void navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 1800);
};
return (
<GlassCard delay={0.05}>
<SectionHeader icon={Smartphone} title="iOS App / Device" 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>
<div>
<p className="text-white text-sm font-medium">{paired ? 'iPhone Paired' : 'No Device Paired'}</p>
<p className="text-xs mt-0.5" style={{ color: 'hsl(var(--muted-fg))' }}>
{paired ? "Ahmeds iPhone 15 Pro" : 'Open Velocity iOS app to connect'}
</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>
: <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>
<div className="flex items-center justify-between">
<p className="text-2xl font-bold tracking-[0.2em] text-white font-mono">{pairingCode}</p>
<button
type="button"
onClick={() => handleCopy(pairingCode)}
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>
</div>
{/* Actions */}
<div className="flex gap-3">
<motion.button
type="button"
onClick={handlePair}
disabled={pairing || paired}
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 } : {}}
>
{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</>
)}
</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={() => { }} />
</div>
</div>
</GlassCard>
);
}
// ── Profile ──────────────────────────────────────────────────────────────────
function ProfileSettings() {
const { user } = useStore();
const initials = user?.name.split(' ').map((n) => n[0]).join('') ?? 'AA';
return (
<GlassCard delay={0.1}>
<SectionHeader icon={User} title="Profile" />
<div className="px-6 pb-6">
<div className="flex items-center gap-4 mb-5 p-4 rounded-xl" style={INNER_SURFACE}>
<div
className="w-14 h-14 rounded-full flex items-center justify-center flex-shrink-0 text-white text-lg font-bold"
style={{ background: 'hsl(var(--accent))', boxShadow: '0 0 20px hsl(var(--accent) / 0.3)' }}
>
{initials}
</div>
<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'}
</p>
</div>
</div>
<div className="space-y-0 -mx-6">
<SettingsRow label="Full Name" description="Your display name">
<DarkInput defaultValue={user?.name} />
</SettingsRow>
<SettingsRow label="Email" description="Notification email">
<DarkInput type="email" defaultValue="ahmed@velocity.re" />
</SettingsRow>
<SettingsRow label="Phone" description="Contact number">
<DarkInput type="tel" defaultValue="+971 50 123 4567" />
</SettingsRow>
</div>
</div>
</GlassCard>
);
}
// ── Notifications ────────────────────────────────────────────────────────────
function NotificationSettings() {
const [s, setS] = useState({ newLeads: true, sentimentAlerts: true, viewings: true, systemUpdates: false, emailDigest: true });
const rows: { key: keyof typeof s; label: string; desc: string }[] = [
{ key: 'newLeads', label: 'New Lead Alerts', desc: 'Get notified when a new lead is captured' },
{ key: 'sentimentAlerts', label: 'Sentiment Alerts', desc: 'Alert when visitor sentiment drops' },
{ key: 'viewings', label: 'Viewing Reminders', desc: 'Reminders for scheduled viewings' },
{ key: 'systemUpdates', label: 'System Updates', desc: 'Notifications about system maintenance' },
{ key: 'emailDigest', label: 'Daily Email Digest', desc: 'Summary of daily activity' },
];
return (
<GlassCard delay={0.15}>
<SectionHeader icon={Bell} title="Notifications" />
<div className="-mx-0">
{rows.map(({ key, label, desc }) => (
<SettingsRow key={key} label={label} description={desc}>
<Toggle enabled={s[key]} onChange={(v) => setS({ ...s, [key]: v })} />
</SettingsRow>
))}
</div>
</GlassCard>
);
}
// ── Security ─────────────────────────────────────────────────────────────────
function SecuritySettings() {
const [twoFactor, setTwoFactor] = useState(true);
const [biometric, setBiometric] = useState(true);
const [timeout, setTimeout_] = useState('30');
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>
<SettingsRow label="Biometric Login" description="Use FaceID for authentication">
<Toggle enabled={biometric} onChange={setBiometric} />
</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>
<SettingsRow label="Session Timeout" description="Auto-logout after inactivity">
<DarkSelect
value={timeout}
onChange={setTimeout_}
options={[
{ value: '15', label: '15 minutes' },
{ value: '30', label: '30 minutes' },
{ value: '60', label: '1 hour' },
{ value: 'never', label: 'Never' },
]}
/>
</SettingsRow>
</div>
</GlassCard>
);
}
// ── Display ──────────────────────────────────────────────────────────────────
function DisplaySettings() {
const [reducedMotion, setReducedMotion] = useState(false);
const [compactMode, setCompactMode] = useState(false);
const [language, setLanguage] = useState('en');
const [timezone, setTimezone] = useState('dxb');
const { currency, setCurrency } = useCurrency();
return (
<GlassCard delay={0.25}>
<SectionHeader icon={Monitor} title="Display" />
<div>
<SettingsRow label="Reduced Motion" description="Minimize animations">
<Toggle enabled={reducedMotion} onChange={setReducedMotion} />
</SettingsRow>
<SettingsRow label="Compact Mode" description="Show more content per screen">
<Toggle enabled={compactMode} onChange={setCompactMode} />
</SettingsRow>
<SettingsRow label="Language" description="Interface language">
<DarkSelect
value={language}
onChange={setLanguage}
options={[
{ value: 'en', label: 'English' },
{ value: 'ar', label: 'العربية' },
]}
/>
</SettingsRow>
<SettingsRow label="Timezone" description="Local time display">
<DarkSelect
value={timezone}
onChange={setTimezone}
options={[
{ value: 'dxb', label: 'Dubai (GMT+4)' },
{ value: 'ruh', label: 'Riyadh (GMT+3)' },
{ value: 'lon', label: 'London (GMT+0)' },
]}
/>
</SettingsRow>
{/* ── Currency ── */}
<SettingsRow
label="Currency"
description="Default currency shown across the entire app"
>
<DarkSelect
value={currency}
onChange={(v) => setCurrency(v as CurrencyCode)}
options={CURRENCY_OPTIONS.map((o) => ({
value: o.code,
label: `${o.flag} ${o.symbol}${o.label}`,
}))}
/>
</SettingsRow>
</div>
</GlassCard>
);
}
// ── Data & Privacy ───────────────────────────────────────────────────────────
function DataSettings() {
const [retention, setRetention] = useState('90');
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>
<SettingsRow label="Data Retention" description="How long to keep visitor data">
<DarkSelect
value={retention}
onChange={setRetention}
options={[
{ value: '30', label: '30 days' },
{ value: '90', label: '90 days' },
{ value: '180', label: '6 months' },
{ value: '365', label: '1 year' },
]}
/>
</SettingsRow>
<SettingsRow label="Export Data" description="Download all your data">
<GhostButton>Export</GhostButton>
</SettingsRow>
<SettingsRow label="Clear Cache" description="Remove temporary files" danger>
<GhostButton danger>Clear</GhostButton>
</SettingsRow>
</div>
</GlassCard>
);
}
// ── About ────────────────────────────────────────────────────────────────────
function AboutSection() {
return (
<GlassCard delay={0.35}>
<SectionHeader icon={Wifi} title="About" />
<div className="px-6 pb-6 text-center">
<div
className="w-16 h-16 rounded-2xl flex items-center justify-center mx-auto mb-4 text-white text-2xl font-bold"
style={{ background: 'hsl(var(--accent))', boxShadow: '0 0 28px hsl(var(--accent) / 0.35)' }}
>
V
</div>
<h4 className="text-white font-semibold text-lg mb-1">Velocity WebOS</h4>
<p className="text-sm mb-4" style={{ color: 'hsl(var(--muted-fg))' }}>Real Estate Operating System</p>
<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>
</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>
</div>
</GlassCard>
);
}
// ── Main ─────────────────────────────────────────────────────────────────────
export function Settings() {
return (
<div className="space-y-4">
{/* Row 1: System + iOS */}
<div className="grid grid-cols-2 gap-4 relative z-40">
<SystemStatusCard />
<IOSConnectionCard />
</div>
{/* Row 2: Profile + Notifications */}
<div className="grid grid-cols-2 gap-4 relative z-30">
<ProfileSettings />
<NotificationSettings />
</div>
{/* Row 3: Security + Display */}
<div className="grid grid-cols-2 gap-4 relative z-20">
<SecuritySettings />
<DisplaySettings />
</div>
{/* Row 4: Data + About */}
<div className="grid grid-cols-2 gap-4 relative z-10">
<DataSettings />
<AboutSection />
</div>
</div>
);
}