520 lines
25 KiB
TypeScript
520 lines
25 KiB
TypeScript
/**
|
||
* @license
|
||
* SPDX-License-Identifier: Apache-2.0
|
||
*/
|
||
|
||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||
import {
|
||
Building2,
|
||
Store,
|
||
Truck,
|
||
CreditCard,
|
||
SlidersHorizontal,
|
||
Users,
|
||
MapPin,
|
||
Phone,
|
||
Mail,
|
||
Check,
|
||
RotateCcw,
|
||
CheckCircle2,
|
||
} from 'lucide-react';
|
||
import { useFiestaAllTenants, useFiestaTenantLocations } from '../services/fiestaQueries';
|
||
import { FIESTA_TENANT_ID, str as fstr, num as fnum, roleName } from '../services/fiestaApi';
|
||
import UsersPanel from './UsersPanel';
|
||
|
||
interface SettingsViewProps {
|
||
tenantId?: number;
|
||
}
|
||
|
||
type TabKey = 'profile' | 'outlets' | 'users' | 'delivery' | 'payment' | 'preferences';
|
||
|
||
/** Locally-persisted merchant preferences (survive reload via localStorage). */
|
||
interface MerchantSettings {
|
||
// Business profile (seeded from live tenant data, then locally editable)
|
||
contactEmail: string;
|
||
contactPhone: string;
|
||
minOrderValue: number;
|
||
// Delivery
|
||
deliveryCharge: number;
|
||
prepMins: number;
|
||
deliveryWindowMins: number;
|
||
cancelWindowSecs: number;
|
||
autoAssignRider: boolean;
|
||
// Payment & tax
|
||
defaultTaxPercent: number;
|
||
codEnabled: boolean;
|
||
onlinePaymentEnabled: boolean;
|
||
// Preferences
|
||
defaultRegion: string;
|
||
defaultNewUserRole: number;
|
||
orderNotifications: boolean;
|
||
lowStockAlerts: boolean;
|
||
dailySummaryEmail: boolean;
|
||
syncInterval: number;
|
||
sandboxMode: boolean;
|
||
}
|
||
|
||
const STORAGE_KEY = 'merchant-settings-v1';
|
||
|
||
const DEFAULTS: MerchantSettings = {
|
||
contactEmail: '',
|
||
contactPhone: '',
|
||
minOrderValue: 0,
|
||
deliveryCharge: 30,
|
||
prepMins: 15,
|
||
deliveryWindowMins: 45,
|
||
cancelWindowSecs: 60,
|
||
autoAssignRider: true,
|
||
defaultTaxPercent: 5,
|
||
codEnabled: true,
|
||
onlinePaymentEnabled: true,
|
||
defaultRegion: 'Coimbatore',
|
||
defaultNewUserRole: 4,
|
||
orderNotifications: true,
|
||
lowStockAlerts: true,
|
||
dailySummaryEmail: false,
|
||
syncInterval: 5,
|
||
sandboxMode: false,
|
||
};
|
||
|
||
function loadSettings(): { settings: MerchantSettings; hadSaved: boolean } {
|
||
try {
|
||
const raw = localStorage.getItem(STORAGE_KEY);
|
||
if (raw) return { settings: { ...DEFAULTS, ...JSON.parse(raw) }, hadSaved: true };
|
||
} catch {
|
||
/* ignore corrupt storage */
|
||
}
|
||
return { settings: { ...DEFAULTS }, hadSaved: false };
|
||
}
|
||
|
||
// ── Small presentational helpers ────────────────────────────────────────────
|
||
function Toggle({ checked, onChange }: { checked: boolean; onChange: () => void }) {
|
||
return (
|
||
<label className="relative inline-flex items-center cursor-pointer shrink-0">
|
||
<input type="checkbox" checked={checked} onChange={onChange} className="sr-only peer" />
|
||
<div className="w-9 h-5 bg-zinc-200 rounded-full peer peer-focus:ring-0 peer-checked:after:translate-x-full after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-zinc-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-[#581c87]" />
|
||
</label>
|
||
);
|
||
}
|
||
|
||
function Row({
|
||
title,
|
||
desc,
|
||
children,
|
||
}: {
|
||
title: string;
|
||
desc?: string;
|
||
children: React.ReactNode;
|
||
}) {
|
||
return (
|
||
<div className="flex justify-between items-center gap-md p-sm bg-zinc-50 rounded-lg border border-[#e2e8f0]/45">
|
||
<div className="min-w-0">
|
||
<h4 className="font-sans font-bold text-xs text-[#0f172a]">{title}</h4>
|
||
{desc && <p className="text-zinc-400 text-[10px] mt-xs">{desc}</p>}
|
||
</div>
|
||
<div className="shrink-0">{children}</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const numberInputCls =
|
||
'w-24 border border-[#e2e8f0] rounded-lg p-1.5 text-right font-semibold text-zinc-700 bg-white outline-none focus:ring-1 focus:ring-[#581c87]';
|
||
const textInputCls =
|
||
'w-full border border-[#e2e8f0] rounded-lg p-sm bg-white outline-none focus:ring-1 focus:ring-[#581c87] text-zinc-700 font-medium';
|
||
const selectCls =
|
||
'border border-[#e2e8f0] bg-white rounded-lg p-1.5 font-semibold text-zinc-700 outline-none cursor-pointer';
|
||
|
||
export default function SettingsView({ tenantId = FIESTA_TENANT_ID }: SettingsViewProps) {
|
||
const [activeTab, setActiveTab] = useState<TabKey>('profile');
|
||
|
||
// Live tenant profile + outlets.
|
||
const tenantsQ = useFiestaAllTenants({ pagesize: 50 });
|
||
const tenant = (tenantsQ.data ?? []).find((t) => Number(t.tenantid) === tenantId) || null;
|
||
const locationsQ = useFiestaTenantLocations(tenantId);
|
||
const outlets = locationsQ.data ?? [];
|
||
|
||
// Persisted preferences.
|
||
const initial = useRef(loadSettings());
|
||
const [form, setForm] = useState<MerchantSettings>(initial.current.settings);
|
||
const [saved, setSaved] = useState<MerchantSettings>(initial.current.settings);
|
||
const [toast, setToast] = useState<string | null>(null);
|
||
|
||
// First-run seeding: if nothing was saved yet, fill contact/min-order/region
|
||
// from the live tenant once it arrives.
|
||
const seededRef = useRef(initial.current.hadSaved);
|
||
useEffect(() => {
|
||
if (seededRef.current || !tenant) return;
|
||
seededRef.current = true;
|
||
const seed = (prev: MerchantSettings): MerchantSettings => ({
|
||
...prev,
|
||
contactEmail: prev.contactEmail || fstr(tenant.primaryemail),
|
||
contactPhone: prev.contactPhone || fstr(tenant.primarycontact),
|
||
minOrderValue: prev.minOrderValue || fnum(tenant.minorder),
|
||
defaultRegion: prev.defaultRegion || fstr(tenant.city) || 'Coimbatore',
|
||
});
|
||
setForm(seed);
|
||
setSaved(seed);
|
||
}, [tenant]);
|
||
|
||
const dirty = useMemo(() => JSON.stringify(form) !== JSON.stringify(saved), [form, saved]);
|
||
|
||
const set = <K extends keyof MerchantSettings>(key: K, value: MerchantSettings[K]) =>
|
||
setForm((f) => ({ ...f, [key]: value }));
|
||
|
||
const handleSave = () => {
|
||
try {
|
||
localStorage.setItem(STORAGE_KEY, JSON.stringify(form));
|
||
} catch {
|
||
/* ignore quota errors */
|
||
}
|
||
setSaved(form);
|
||
setToast('Settings saved');
|
||
window.setTimeout(() => setToast(null), 2200);
|
||
};
|
||
|
||
const handleReset = () => setForm(saved);
|
||
|
||
const tabs: Array<{ key: TabKey; label: string; icon: typeof Building2 }> = [
|
||
{ key: 'profile', label: 'Business Profile', icon: Building2 },
|
||
{ key: 'outlets', label: 'Outlets', icon: Store },
|
||
{ key: 'users', label: 'Users & Access', icon: Users },
|
||
{ key: 'delivery', label: 'Delivery', icon: Truck },
|
||
{ key: 'payment', label: 'Payment & Tax', icon: CreditCard },
|
||
{ key: 'preferences', label: 'Preferences', icon: SlidersHorizontal },
|
||
];
|
||
|
||
const roleOptions = [1, 2, 3, 4, 6];
|
||
|
||
return (
|
||
<div className="space-y-lg animate-in fade-in duration-300 relative">
|
||
{/* Header */}
|
||
<div>
|
||
<h1 className="font-sans font-bold text-2xl tracking-tight text-[#0f172a]">Settings</h1>
|
||
<p className="text-zinc-500 font-sans text-xs mt-1">
|
||
Manage your store profile, outlets, delivery, payments, and workspace preferences.
|
||
</p>
|
||
<div className="mt-1.5">
|
||
{tenantsQ.isLoading ? (
|
||
<span className="inline-flex items-center gap-1 text-[10px] font-semibold text-zinc-400 uppercase tracking-wide">
|
||
<span className="w-1.5 h-1.5 rounded-full bg-zinc-300 animate-pulse" /> Loading live profile…
|
||
</span>
|
||
) : tenant ? (
|
||
<span className="inline-flex items-center gap-1 text-[10px] font-semibold text-emerald-600 uppercase tracking-wide">
|
||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500" /> Live · {fstr(tenant.tenantname)} · Tenant {tenantId}
|
||
</span>
|
||
) : (
|
||
<span className="inline-flex items-center gap-1 text-[10px] font-semibold text-rose-600 uppercase tracking-wide">
|
||
<span className="w-1.5 h-1.5 rounded-full bg-rose-500" /> Tenant profile unavailable
|
||
</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-gutter items-start">
|
||
{/* Tab rail */}
|
||
<nav className="lg:col-span-1 bg-white border border-[#e2e8f0] rounded-xl p-2 shadow-sm flex lg:flex-col gap-1 overflow-x-auto">
|
||
{tabs.map((t) => {
|
||
const Icon = t.icon;
|
||
const active = activeTab === t.key;
|
||
return (
|
||
<button
|
||
key={t.key}
|
||
onClick={() => setActiveTab(t.key)}
|
||
className={`flex items-center gap-sm px-sm py-2 rounded-lg text-xs font-semibold transition-colors whitespace-nowrap cursor-pointer ${
|
||
active ? 'bg-[#faf5ff] text-[#581c87]' : 'text-zinc-600 hover:bg-zinc-50'
|
||
}`}
|
||
>
|
||
<Icon size={15} className={active ? 'text-[#581c87]' : 'text-zinc-400'} />
|
||
{t.label}
|
||
</button>
|
||
);
|
||
})}
|
||
</nav>
|
||
|
||
{/* Panel */}
|
||
<div className="lg:col-span-3 space-y-gutter text-xs font-sans">
|
||
{activeTab === 'profile' && (
|
||
<div className="bg-white border border-[#e2e8f0] p-md rounded-xl shadow-sm space-y-md">
|
||
<span className="text-[10px] font-bold text-zinc-400 uppercase tracking-widest block pb-xs border-b border-[#f1f5f9]">
|
||
Business Profile
|
||
</span>
|
||
|
||
{/* Live identity (read-only) */}
|
||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-sm">
|
||
<div className="p-sm bg-zinc-50 rounded-lg border border-[#e2e8f0]/45">
|
||
<span className="text-[9px] text-zinc-400 uppercase font-bold tracking-widest">Store Name</span>
|
||
<p className="font-bold text-[#0f172a] mt-0.5">{fstr(tenant?.tenantname) || '—'}</p>
|
||
</div>
|
||
<div className="p-sm bg-zinc-50 rounded-lg border border-[#e2e8f0]/45">
|
||
<span className="text-[9px] text-zinc-400 uppercase font-bold tracking-widest">Legal / Company</span>
|
||
<p className="font-bold text-[#0f172a] mt-0.5">{fstr(tenant?.companyname) || '—'}</p>
|
||
</div>
|
||
<div className="p-sm bg-zinc-50 rounded-lg border border-[#e2e8f0]/45">
|
||
<span className="text-[9px] text-zinc-400 uppercase font-bold tracking-widest">Category</span>
|
||
<p className="font-bold text-[#0f172a] mt-0.5">{fstr(tenant?.subcategoryname) || `Category ${fnum(tenant?.categoryid)}`}</p>
|
||
</div>
|
||
<div className="p-sm bg-zinc-50 rounded-lg border border-[#e2e8f0]/45 flex items-center justify-between">
|
||
<div>
|
||
<span className="text-[9px] text-zinc-400 uppercase font-bold tracking-widest">Account Status</span>
|
||
<p className="font-bold text-[#0f172a] mt-0.5">{fstr(tenant?.status) || '—'}</p>
|
||
</div>
|
||
<span className={`px-1.5 py-0.5 rounded text-[9px] font-bold uppercase ${
|
||
fstr(tenant?.status).toLowerCase() === 'active'
|
||
? 'text-emerald-700 bg-emerald-100'
|
||
: 'text-zinc-500 bg-zinc-200'
|
||
}`}>
|
||
{fnum(tenant?.approved) === 1 ? 'Approved' : 'Pending'}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="p-sm bg-zinc-50 rounded-lg border border-[#e2e8f0]/45 flex items-start gap-sm">
|
||
<MapPin size={13} className="text-zinc-400 shrink-0 mt-0.5" />
|
||
<div>
|
||
<span className="text-[9px] text-zinc-400 uppercase font-bold tracking-widest">Registered Address</span>
|
||
<p className="text-zinc-700 font-medium mt-0.5 leading-relaxed">
|
||
{fstr(tenant?.address) || '—'}
|
||
{tenant?.city ? ` · ${fstr(tenant.city)}, ${fstr(tenant.state)} ${fstr(tenant.postcode)}` : ''}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Editable contact (persisted locally) */}
|
||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-sm pt-xs">
|
||
<div className="space-y-1">
|
||
<label className="font-bold text-zinc-500 uppercase tracking-widest text-[9px] flex items-center gap-1">
|
||
<Mail size={11} /> Contact Email
|
||
</label>
|
||
<input
|
||
type="email"
|
||
value={form.contactEmail}
|
||
onChange={(e) => set('contactEmail', e.target.value)}
|
||
className={textInputCls}
|
||
placeholder="store@example.com"
|
||
/>
|
||
</div>
|
||
<div className="space-y-1">
|
||
<label className="font-bold text-zinc-500 uppercase tracking-widest text-[9px] flex items-center gap-1">
|
||
<Phone size={11} /> Contact Phone
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={form.contactPhone}
|
||
onChange={(e) => set('contactPhone', e.target.value)}
|
||
className={textInputCls}
|
||
placeholder="9876543210"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<p className="text-[10px] text-zinc-400">
|
||
Identity fields above are read live from your tenant record. Contact details are saved to this workspace.
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
{activeTab === 'outlets' && (
|
||
<div className="bg-white border border-[#e2e8f0] p-md rounded-xl shadow-sm space-y-md">
|
||
<div className="flex justify-between items-center pb-xs border-b border-[#f1f5f9]">
|
||
<span className="text-[10px] font-bold text-zinc-400 uppercase tracking-widest">Outlet Locations</span>
|
||
<span className="text-[10px] text-[#581c87] font-bold bg-purple-50 px-2 py-0.5 rounded border border-purple-100">
|
||
{locationsQ.isLoading ? 'Loading…' : `${outlets.length} outlet${outlets.length === 1 ? '' : 's'}`}
|
||
</span>
|
||
</div>
|
||
|
||
{locationsQ.isLoading ? (
|
||
<div className="text-center py-lg text-zinc-400">Loading live outlets…</div>
|
||
) : outlets.length === 0 ? (
|
||
<div className="text-center py-lg text-zinc-400">No outlets found for this tenant.</div>
|
||
) : (
|
||
<div className="space-y-sm max-h-[28rem] overflow-y-auto">
|
||
{outlets.map((loc, i) => (
|
||
<div key={fstr(loc.locationid) || i} className="p-sm border border-[#e2e8f0] rounded-lg bg-[#f8fafc]/40">
|
||
<div className="flex justify-between items-start gap-md">
|
||
<div className="min-w-0">
|
||
<p className="font-bold text-[#0f172a] truncate">{fstr(loc.locationname)}</p>
|
||
<p className="text-[10px] text-zinc-500 mt-0.5 flex items-center gap-1">
|
||
<MapPin size={10} className="shrink-0 text-zinc-400" />
|
||
<span className="truncate">{fstr(loc.suburb)}, {fstr(loc.city)} {fstr(loc.postcode)}</span>
|
||
</p>
|
||
</div>
|
||
<span className={`shrink-0 px-1.5 py-0.5 rounded text-[9px] font-bold uppercase ${
|
||
fstr(loc.status).toLowerCase() === 'active'
|
||
? 'text-emerald-600 bg-emerald-50 border border-emerald-100'
|
||
: 'text-zinc-500 bg-zinc-100'
|
||
}`}>
|
||
{fstr(loc.status) || '—'}
|
||
</span>
|
||
</div>
|
||
<div className="grid grid-cols-3 gap-2 mt-sm text-center">
|
||
<div className="bg-white border border-[#e2e8f0]/60 rounded p-1">
|
||
<p className="text-[9px] text-zinc-400 uppercase font-bold">Hours</p>
|
||
<p className="font-mono font-semibold text-zinc-700 text-[10px] mt-0.5">
|
||
{fstr(loc.opentime).slice(11, 16) || '—'}–{fstr(loc.closetime).slice(11, 16) || '—'}
|
||
</p>
|
||
</div>
|
||
<div className="bg-white border border-[#e2e8f0]/60 rounded p-1">
|
||
<p className="text-[9px] text-zinc-400 uppercase font-bold">Radius</p>
|
||
<p className="font-mono font-semibold text-zinc-700 text-[10px] mt-0.5">{fnum(loc.deliveryradius)} m</p>
|
||
</div>
|
||
<div className="bg-white border border-[#e2e8f0]/60 rounded p-1">
|
||
<p className="text-[9px] text-zinc-400 uppercase font-bold">ETA</p>
|
||
<p className="font-mono font-semibold text-zinc-700 text-[10px] mt-0.5">{fnum(loc.deliverymins)} min</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
<p className="text-[10px] text-zinc-400">Outlets are read live from your tenant. Add or edit them in the Stores section.</p>
|
||
</div>
|
||
)}
|
||
|
||
{activeTab === 'users' && (
|
||
<UsersPanel tenantId={tenantId} defaultNewUserRole={form.defaultNewUserRole} />
|
||
)}
|
||
|
||
{activeTab === 'delivery' && (
|
||
<div className="bg-white border border-[#e2e8f0] p-md rounded-xl shadow-sm space-y-md">
|
||
<span className="text-[10px] font-bold text-zinc-400 uppercase tracking-widest block pb-xs border-b border-[#f1f5f9]">
|
||
Delivery Settings
|
||
</span>
|
||
<div className="space-y-sm">
|
||
<Row title="Default Delivery Charge" desc="Flat fee added to each delivery order.">
|
||
<div className="flex items-center gap-1">
|
||
<span className="text-zinc-400 font-bold">₹</span>
|
||
<input type="number" min={0} value={form.deliveryCharge}
|
||
onChange={(e) => set('deliveryCharge', Number(e.target.value))} className={numberInputCls} />
|
||
</div>
|
||
</Row>
|
||
<Row title="Preparation Time" desc="Minutes a store needs before pickup.">
|
||
<input type="number" min={0} value={form.prepMins}
|
||
onChange={(e) => set('prepMins', Number(e.target.value))} className={numberInputCls} />
|
||
</Row>
|
||
<Row title="Delivery Window" desc="Target minutes from dispatch to doorstep.">
|
||
<input type="number" min={0} value={form.deliveryWindowMins}
|
||
onChange={(e) => set('deliveryWindowMins', Number(e.target.value))} className={numberInputCls} />
|
||
</Row>
|
||
<Row title="Cancellation Window" desc="Seconds a customer can cancel for free.">
|
||
<input type="number" min={0} value={form.cancelWindowSecs}
|
||
onChange={(e) => set('cancelWindowSecs', Number(e.target.value))} className={numberInputCls} />
|
||
</Row>
|
||
<Row title="Auto-assign Rider" desc="Automatically dispatch the nearest available rider.">
|
||
<Toggle checked={form.autoAssignRider} onChange={() => set('autoAssignRider', !form.autoAssignRider)} />
|
||
</Row>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{activeTab === 'payment' && (
|
||
<div className="bg-white border border-[#e2e8f0] p-md rounded-xl shadow-sm space-y-md">
|
||
<span className="text-[10px] font-bold text-zinc-400 uppercase tracking-widest block pb-xs border-b border-[#f1f5f9]">
|
||
Payment & Tax
|
||
</span>
|
||
<div className="space-y-sm">
|
||
<Row title="Default Tax Rate" desc="Applied to taxable catalogue items.">
|
||
<div className="flex items-center gap-1">
|
||
<input type="number" min={0} max={100} value={form.defaultTaxPercent}
|
||
onChange={(e) => set('defaultTaxPercent', Number(e.target.value))} className={numberInputCls} />
|
||
<span className="text-zinc-400 font-bold">%</span>
|
||
</div>
|
||
</Row>
|
||
<Row title="Minimum Order Value" desc="Smallest order a customer can place.">
|
||
<div className="flex items-center gap-1">
|
||
<span className="text-zinc-400 font-bold">₹</span>
|
||
<input type="number" min={0} value={form.minOrderValue}
|
||
onChange={(e) => set('minOrderValue', Number(e.target.value))} className={numberInputCls} />
|
||
</div>
|
||
</Row>
|
||
<Row title="Cash on Delivery" desc="Allow customers to pay on delivery.">
|
||
<Toggle checked={form.codEnabled} onChange={() => set('codEnabled', !form.codEnabled)} />
|
||
</Row>
|
||
<Row title="Online Payments" desc="Accept UPI / card / wallet at checkout.">
|
||
<Toggle checked={form.onlinePaymentEnabled} onChange={() => set('onlinePaymentEnabled', !form.onlinePaymentEnabled)} />
|
||
</Row>
|
||
<div className="p-sm bg-purple-50 border border-purple-100 rounded-lg text-[#581c87] text-[11px] font-medium">
|
||
Live tenant payment configuration code: <strong>{fnum(tenant?.paymenttype) || '—'}</strong>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{activeTab === 'preferences' && (
|
||
<div className="bg-white border border-[#e2e8f0] p-md rounded-xl shadow-sm space-y-md">
|
||
<span className="text-[10px] font-bold text-zinc-400 uppercase tracking-widest block pb-xs border-b border-[#f1f5f9]">
|
||
Workspace Preferences
|
||
</span>
|
||
<div className="space-y-sm">
|
||
<Row title="Default Region" desc="Region applied to new outlets and reports.">
|
||
<input type="text" value={form.defaultRegion}
|
||
onChange={(e) => set('defaultRegion', e.target.value)} className={`${numberInputCls} w-40 text-left`} />
|
||
</Row>
|
||
<Row title="Default Role for New Users" desc="Pre-selected role in the Add User dialog.">
|
||
<select value={form.defaultNewUserRole}
|
||
onChange={(e) => set('defaultNewUserRole', Number(e.target.value))} className={selectCls}>
|
||
{roleOptions.map((r) => (
|
||
<option key={r} value={r}>{roleName(r)}</option>
|
||
))}
|
||
</select>
|
||
</Row>
|
||
<Row title="Data Sync Interval" desc="How often live data refreshes from the API.">
|
||
<select value={form.syncInterval}
|
||
onChange={(e) => set('syncInterval', Number(e.target.value))} className={selectCls}>
|
||
<option value={1}>Every 1 min</option>
|
||
<option value={5}>Every 5 mins</option>
|
||
<option value={15}>Every 15 mins</option>
|
||
<option value={30}>Every 30 mins</option>
|
||
</select>
|
||
</Row>
|
||
<Row title="Order Notifications" desc="Alert on every new incoming order.">
|
||
<Toggle checked={form.orderNotifications} onChange={() => set('orderNotifications', !form.orderNotifications)} />
|
||
</Row>
|
||
<Row title="Low-stock Alerts" desc="Notify when an SKU drops below threshold.">
|
||
<Toggle checked={form.lowStockAlerts} onChange={() => set('lowStockAlerts', !form.lowStockAlerts)} />
|
||
</Row>
|
||
<Row title="Daily Summary Email" desc="Email a closing-hours performance digest.">
|
||
<Toggle checked={form.dailySummaryEmail} onChange={() => set('dailySummaryEmail', !form.dailySummaryEmail)} />
|
||
</Row>
|
||
<Row title="Sandbox Mode" desc="Simulate warning states without affecting live ops.">
|
||
<Toggle checked={form.sandboxMode} onChange={() => set('sandboxMode', !form.sandboxMode)} />
|
||
</Row>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Save / Reset — lives with the settings card, not pinned to the screen.
|
||
Hidden on the Users tab, which manages accounts via the live API. */}
|
||
<div className={`bg-white border border-[#e2e8f0] rounded-xl p-md shadow-sm flex-col sm:flex-row sm:items-center justify-between gap-sm ${activeTab === 'users' ? 'hidden' : 'flex'}`}>
|
||
<span className={`text-xs font-medium ${dirty ? 'text-amber-600' : 'text-zinc-400'}`}>
|
||
{dirty ? '● You have unsaved changes' : 'All changes saved'}
|
||
</span>
|
||
<div className="flex gap-sm">
|
||
<button
|
||
onClick={handleReset}
|
||
disabled={!dirty}
|
||
className="px-4 py-2 border border-[#e2e8f0] rounded-lg text-xs font-semibold text-zinc-600 hover:bg-zinc-50 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1.5"
|
||
>
|
||
<RotateCcw size={13} /> Reset
|
||
</button>
|
||
<button
|
||
onClick={handleSave}
|
||
disabled={!dirty}
|
||
className="px-4 py-2 bg-[#581c87] text-white rounded-lg text-xs font-bold hover:bg-purple-800 cursor-pointer shadow-sm disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1.5"
|
||
>
|
||
<Check size={13} /> Save Changes
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Toast */}
|
||
{toast && (
|
||
<div className="fixed bottom-md right-md z-[130] bg-[#0f172a] text-white px-4 py-2.5 rounded-lg shadow-2xl flex items-center gap-2 text-xs font-semibold animate-in slide-in-from-bottom-2 fade-in duration-200">
|
||
<CheckCircle2 size={15} className="text-emerald-400" />
|
||
{toast}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|