udpates on the ui changesand api integration
This commit is contained in:
@@ -14,14 +14,13 @@ import {
|
||||
MapPin,
|
||||
Phone,
|
||||
Mail,
|
||||
Check,
|
||||
RotateCcw,
|
||||
CheckCircle2,
|
||||
Plus
|
||||
} from 'lucide-react';
|
||||
import { useFiestaAllTenants, useFiestaTenantLocations } from '../services/fiestaQueries';
|
||||
import { useAppRoles } from '../services/queries';
|
||||
import { FIESTA_TENANT_ID, str as fstr, num as fnum, roleName } from '../services/fiestaApi';
|
||||
import UsersPanel from './UsersPanel';
|
||||
import AwaitingApi from './AwaitingApi';
|
||||
|
||||
interface SettingsViewProps {
|
||||
tenantId?: number;
|
||||
@@ -55,8 +54,6 @@ interface MerchantSettings {
|
||||
sandboxMode: boolean;
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'merchant-settings-v1';
|
||||
|
||||
const DEFAULTS: MerchantSettings = {
|
||||
contactEmail: '',
|
||||
contactPhone: '',
|
||||
@@ -78,28 +75,6 @@ const DEFAULTS: MerchantSettings = {
|
||||
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 };
|
||||
}
|
||||
|
||||
// Localized fallback dataset to replace generic Faker test data with realistic Coimbatore outlets
|
||||
const LOCAL_OUTLETS_DATA = [
|
||||
{ name: 'Ragul Stores - Gandhipuram Hub', suburb: 'Gandhipuram', city: 'Coimbatore', postcode: '641018', radius: 4500, mins: 30 },
|
||||
{ name: 'Ragul Stores - Peelamedu Hub', suburb: 'Peelamedu', city: 'Coimbatore', postcode: '641004', radius: 3500, mins: 25 },
|
||||
{ name: 'Ragul Stores - RS Puram Hub', suburb: 'RS Puram', city: 'Coimbatore', postcode: '641002', radius: 5000, mins: 35 },
|
||||
{ name: 'Ragul Stores - Saravanampatti Outlet', suburb: 'Saravanampatti', city: 'Coimbatore', postcode: '641035', radius: 6000, mins: 40 },
|
||||
{ name: 'Ragul Stores - Singanallur Outlet', suburb: 'Singanallur', city: 'Coimbatore', postcode: '641005', radius: 4000, mins: 30 },
|
||||
{ name: 'Ragul Stores - Vadavalli Hub', suburb: 'Vadavalli', city: 'Coimbatore', postcode: '641046', radius: 3000, mins: 20 },
|
||||
{ name: 'Ragul Stores - Ramanathapuram Hub', suburb: 'Ramanathapuram', city: 'Coimbatore', postcode: '641045', radius: 4500, mins: 30 },
|
||||
{ name: 'Ragul Stores - Town Hall Outlet', suburb: 'Town Hall', city: 'Coimbatore', postcode: '641001', radius: 3500, mins: 25 },
|
||||
];
|
||||
|
||||
const formatFriendlyTime = (timeStr: string) => {
|
||||
try {
|
||||
if (timeStr.includes('T')) {
|
||||
@@ -127,15 +102,6 @@ const formatFriendlyTime = (timeStr: string) => {
|
||||
};
|
||||
|
||||
/// ── Small presentational helpers ────────────────────────────────────────────
|
||||
function Toggle({ checked, onChange }: { checked: boolean; onChange: () => void }) {
|
||||
return (
|
||||
<label className="relative inline-flex items-center cursor-pointer shrink-0 select-none group">
|
||||
<input type="checkbox" checked={checked} onChange={onChange} className="sr-only peer" />
|
||||
<div className="w-11 h-6 bg-slate-200 peer-focus:outline-none rounded-full transition-all duration-300 peer-checked:bg-purple-650 after:content-[''] after:absolute after:top-[3.5px] after:left-[4px] after:bg-white after:rounded-full after:h-4.5 after:w-4.5 after:transition-all after:duration-300 peer-checked:after:translate-x-5 shadow-sm group-active:after:w-5.5 peer-checked:group-active:after:translate-x-4" />
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
function Row({
|
||||
title,
|
||||
desc,
|
||||
@@ -165,73 +131,48 @@ export default function SettingsView({ tenantId = FIESTA_TENANT_ID }: SettingsVi
|
||||
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);
|
||||
// Application roles (Hasura) — drives the role dropdowns.
|
||||
const rolesQ = useAppRoles();
|
||||
|
||||
// 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);
|
||||
// In-session workspace preferences. These have NO merchant-settings backend
|
||||
// (see [R6]) so they are not persisted; the operational controls that would
|
||||
// need persistence show an AwaitingApi notice instead of saving silently.
|
||||
const [form, setForm] = useState<MerchantSettings>({ ...DEFAULTS });
|
||||
|
||||
// First-run seeding: fill region/role defaults from the live tenant once it
|
||||
// arrives (used at runtime by the Add User dialog / region label).
|
||||
const seededRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (seededRef.current || !tenant) return;
|
||||
seededRef.current = true;
|
||||
const seed = (prev: MerchantSettings): MerchantSettings => ({
|
||||
setForm((prev) => ({
|
||||
...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]);
|
||||
|
||||
// Live outlets only — no fabricated fallback. Render whatever the API returns.
|
||||
const cleanOutlets = useMemo(() => {
|
||||
return outlets.map((loc, idx) => {
|
||||
// If the location name is a mock name (doesn't contain store context), replace with Coimbatore locations
|
||||
const nameStr = fstr(loc.locationname);
|
||||
const isMockTest = !nameStr.toLowerCase().includes('stores') &&
|
||||
!nameStr.toLowerCase().includes('outlet') &&
|
||||
!nameStr.toLowerCase().includes('hub') &&
|
||||
!nameStr.toLowerCase().includes('ragul');
|
||||
|
||||
const localData = LOCAL_OUTLETS_DATA[idx % LOCAL_OUTLETS_DATA.length];
|
||||
|
||||
return {
|
||||
locationid: fstr(loc.locationid) || String(1090 + idx),
|
||||
locationname: isMockTest ? localData.name : nameStr,
|
||||
suburb: isMockTest ? localData.suburb : (fstr(loc.suburb) || localData.suburb),
|
||||
city: isMockTest ? localData.city : (fstr(loc.city) || localData.city),
|
||||
postcode: isMockTest ? localData.postcode : (fstr(loc.postcode) || localData.postcode),
|
||||
status: fstr(loc.status) || 'Active',
|
||||
opentime: fstr(loc.opentime) || '2026-06-04T09:00:00Z',
|
||||
closetime: fstr(loc.closetime) || '2026-06-04T22:00:00Z',
|
||||
deliverymins: isMockTest ? localData.mins : (fnum(loc.deliverymins) || localData.mins),
|
||||
deliveryradius: isMockTest ? localData.radius : (fnum(loc.deliveryradius) || localData.radius),
|
||||
};
|
||||
});
|
||||
return outlets.map((loc, idx) => ({
|
||||
locationid: fstr(loc.locationid) || String(idx),
|
||||
locationname: fstr(loc.locationname) || '—',
|
||||
suburb: fstr(loc.suburb),
|
||||
city: fstr(loc.city),
|
||||
postcode: fstr(loc.postcode),
|
||||
status: fstr(loc.status) || '—',
|
||||
opentime: fstr(loc.opentime),
|
||||
closetime: fstr(loc.closetime),
|
||||
deliverymins: fnum(loc.deliverymins),
|
||||
deliveryradius: fnum(loc.deliveryradius),
|
||||
}));
|
||||
}, [outlets]);
|
||||
|
||||
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 },
|
||||
@@ -241,7 +182,23 @@ export default function SettingsView({ tenantId = FIESTA_TENANT_ID }: SettingsVi
|
||||
{ key: 'preferences', label: 'Preferences', icon: SlidersHorizontal },
|
||||
];
|
||||
|
||||
const roleOptions = [1, 2, 3, 4, 6];
|
||||
// Build role options from the live app-roles API; fall back to the known
|
||||
// numeric roles + roleName() helper when the API has no rows/names.
|
||||
const roleOptions = useMemo<Array<{ id: number; name: string }>>(() => {
|
||||
const rows = rolesQ.data ?? [];
|
||||
const mapped = rows
|
||||
.map((r) => {
|
||||
const id = fnum((r as Record<string, unknown>).roleid);
|
||||
const name =
|
||||
fstr((r as Record<string, unknown>).rolename) ||
|
||||
fstr((r as Record<string, unknown>).name) ||
|
||||
roleName(id);
|
||||
return { id, name };
|
||||
})
|
||||
.filter((r) => r.id > 0);
|
||||
if (mapped.length) return mapped;
|
||||
return [1, 2, 3, 4, 6].map((id) => ({ id, name: roleName(id) }));
|
||||
}, [rolesQ.data]);
|
||||
|
||||
return (
|
||||
<div className="space-y-lg animate-in fade-in duration-300 relative font-sans text-slate-700">
|
||||
@@ -398,7 +355,7 @@ export default function SettingsView({ tenantId = FIESTA_TENANT_ID }: SettingsVi
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Editable contact (persisted locally) */}
|
||||
{/* Customer support contacts — live (read-only) tenant values. */}
|
||||
<div className="space-y-sm">
|
||||
<span className="text-xs font-bold text-slate-500 uppercase tracking-wider block">Customer Support & Contacts</span>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-md mt-2">
|
||||
@@ -408,10 +365,10 @@ export default function SettingsView({ tenantId = FIESTA_TENANT_ID }: SettingsVi
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
value={form.contactEmail}
|
||||
onChange={(e) => set('contactEmail', e.target.value)}
|
||||
className="w-full border border-slate-200 rounded-xl py-3 px-4 bg-slate-50/40 hover:bg-slate-100 focus:bg-white outline-none focus:border-purple-500 focus:ring-4 focus:ring-purple-500/10 transition-all text-slate-800 font-semibold text-sm shadow-sm"
|
||||
placeholder="store@example.com"
|
||||
value={fstr(tenant?.primaryemail) || ''}
|
||||
readOnly
|
||||
className="w-full border border-slate-200 rounded-xl py-3 px-4 bg-slate-50/40 outline-none transition-all text-slate-800 font-semibold text-sm shadow-sm"
|
||||
placeholder="—"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
@@ -420,10 +377,10 @@ export default function SettingsView({ tenantId = FIESTA_TENANT_ID }: SettingsVi
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={form.contactPhone}
|
||||
onChange={(e) => set('contactPhone', e.target.value)}
|
||||
className="w-full border border-slate-200 rounded-xl py-3 px-4 bg-slate-50/40 hover:bg-slate-100 focus:bg-white outline-none focus:border-purple-500 focus:ring-4 focus:ring-purple-500/10 transition-all text-slate-800 font-semibold text-sm shadow-sm"
|
||||
placeholder="9876543210"
|
||||
value={fstr(tenant?.primarycontact) || ''}
|
||||
readOnly
|
||||
className="w-full border border-slate-200 rounded-xl py-3 px-4 bg-slate-50/40 outline-none transition-all text-slate-800 font-semibold text-sm shadow-sm"
|
||||
placeholder="—"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -446,7 +403,7 @@ export default function SettingsView({ tenantId = FIESTA_TENANT_ID }: SettingsVi
|
||||
{locationsQ.isLoading ? (
|
||||
<div className="text-center py-lg text-slate-400 font-medium text-sm">Loading live outlets…</div>
|
||||
) : cleanOutlets.length === 0 ? (
|
||||
<div className="text-center py-lg text-slate-400 font-medium text-sm">No outlets found for this store.</div>
|
||||
<div className="text-center py-lg text-slate-400 font-medium text-sm">No outlets configured yet.</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-md max-h-[38rem] overflow-y-auto pr-1 scrollbar-thin">
|
||||
{cleanOutlets.map((loc, i) => (
|
||||
@@ -462,7 +419,7 @@ export default function SettingsView({ tenantId = FIESTA_TENANT_ID }: SettingsVi
|
||||
<p className="font-bold text-slate-800 text-sm truncate leading-tight group-hover:text-purple-950 transition-colors">{loc.locationname}</p>
|
||||
<p className="text-xs text-slate-450 mt-1.5 flex items-center gap-1">
|
||||
<MapPin size={12} className="shrink-0 text-slate-400" />
|
||||
<span className="truncate">{loc.suburb}, {loc.city}</span>
|
||||
<span className="truncate">{[loc.suburb, loc.city].filter(Boolean).join(', ') || '—'}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -480,20 +437,22 @@ export default function SettingsView({ tenantId = FIESTA_TENANT_ID }: SettingsVi
|
||||
<div className="space-y-1">
|
||||
<span className="text-[10px] text-slate-450 uppercase font-bold block">Delivery Range</span>
|
||||
<p className="font-bold text-slate-700 text-xs">
|
||||
Up to {loc.deliveryradius / 1000} km
|
||||
{loc.deliveryradius ? `Up to ${loc.deliveryradius / 1000} km` : '—'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<span className="text-[10px] text-slate-455 uppercase font-bold block">Delivery Speed</span>
|
||||
<p className="font-bold text-slate-700 text-xs">
|
||||
{loc.deliverymins} mins avg
|
||||
{loc.deliverymins ? `${loc.deliverymins} mins avg` : '—'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-1 col-span-2 border-t border-slate-100 pt-2 mt-1">
|
||||
<span className="text-[10px] text-slate-455 uppercase font-bold block">Opening Hours</span>
|
||||
<p className="font-bold text-slate-750 text-xs flex items-center gap-1.5 mt-0.5">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500" />
|
||||
Open: {formatFriendlyTime(loc.opentime)} – {formatFriendlyTime(loc.closetime)}
|
||||
{loc.opentime && loc.closetime
|
||||
? `Open: ${formatFriendlyTime(loc.opentime)} – ${formatFriendlyTime(loc.closetime)}`
|
||||
: 'Hours not set'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -511,144 +470,47 @@ export default function SettingsView({ tenantId = FIESTA_TENANT_ID }: SettingsVi
|
||||
|
||||
{activeTab === 'delivery' && (
|
||||
<div className="bg-white border border-slate-200/60 p-6 rounded-2xl shadow-sm space-y-lg animate-in fade-in duration-200">
|
||||
{/* Group 1: Order Prep & Timings */}
|
||||
<div className="space-y-sm">
|
||||
<span className="text-xs font-bold text-purple-800 bg-purple-50/70 border border-purple-100/50 px-3 py-1 rounded-full uppercase tracking-wider">
|
||||
Order Prep & Timings
|
||||
</span>
|
||||
<div className="divide-y divide-slate-100/70 mt-2">
|
||||
<Row title="Preparation Time" desc="Minutes a store needs before pickup.">
|
||||
<div className="relative rounded-xl shadow-sm">
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
value={form.prepMins}
|
||||
onChange={(e) => set('prepMins', Number(e.target.value))}
|
||||
className="w-28 pr-9 pl-4 py-2 border border-slate-200 rounded-xl text-right font-bold text-slate-700 bg-slate-50/40 hover:bg-slate-50 focus:bg-white focus:outline-none focus:border-purple-500 focus:ring-2 focus:ring-purple-500/10 transition-all text-sm font-mono"
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
|
||||
<span className="text-slate-400 font-semibold text-[10px] uppercase">min</span>
|
||||
</div>
|
||||
</div>
|
||||
</Row>
|
||||
<Row title="Delivery Window" desc="Estimated delivery time from store to customer.">
|
||||
<div className="relative rounded-xl shadow-sm">
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
value={form.deliveryWindowMins}
|
||||
onChange={(e) => set('deliveryWindowMins', Number(e.target.value))}
|
||||
className="w-28 pr-9 pl-4 py-2 border border-slate-200 rounded-xl text-right font-bold text-slate-700 bg-slate-50/40 hover:bg-slate-50 focus:bg-white focus:outline-none focus:border-purple-500 focus:ring-2 focus:ring-purple-500/10 transition-all text-sm font-mono"
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
|
||||
<span className="text-slate-400 font-semibold text-[10px] uppercase">min</span>
|
||||
</div>
|
||||
</div>
|
||||
</Row>
|
||||
<Row title="Cancellation Window" desc="Seconds a customer can cancel for free.">
|
||||
<div className="relative rounded-xl shadow-sm">
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
value={form.cancelWindowSecs}
|
||||
onChange={(e) => set('cancelWindowSecs', Number(e.target.value))}
|
||||
className="w-28 pr-9 pl-4 py-2 border border-slate-200 rounded-xl text-right font-bold text-slate-700 bg-slate-50/40 hover:bg-slate-50 focus:bg-white focus:outline-none focus:border-purple-500 focus:ring-2 focus:ring-purple-500/10 transition-all text-sm font-mono"
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
|
||||
<span className="text-slate-400 font-semibold text-[10px] uppercase">sec</span>
|
||||
</div>
|
||||
</div>
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Group 2: Delivery Charges & Dispatch */}
|
||||
<div className="space-y-sm">
|
||||
<span className="text-xs font-bold text-purple-800 bg-purple-50/70 border border-purple-100/50 px-3 py-1 rounded-full uppercase tracking-wider">
|
||||
Delivery Charges & Dispatch
|
||||
</span>
|
||||
<div className="divide-y divide-slate-100/70 mt-2">
|
||||
<Row title="Default Delivery Charge" desc="Flat fee added to each delivery order.">
|
||||
<div className="relative rounded-xl shadow-sm">
|
||||
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<span className="text-slate-400 font-bold text-sm">₹</span>
|
||||
</div>
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
value={form.deliveryCharge}
|
||||
onChange={(e) => set('deliveryCharge', Number(e.target.value))}
|
||||
className="w-28 pl-7 pr-4 py-2 border border-slate-200 rounded-xl text-right font-bold text-slate-700 bg-slate-50/40 hover:bg-slate-50 focus:bg-white focus:outline-none focus:border-purple-500 focus:ring-2 focus:ring-purple-500/10 transition-all text-sm font-mono"
|
||||
/>
|
||||
</div>
|
||||
</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>
|
||||
<span className="text-xs font-bold text-slate-450 uppercase tracking-widest block">Delivery</span>
|
||||
<h2 className="text-xl font-bold text-slate-900 mt-1">Order Prep, Timings & Dispatch</h2>
|
||||
</div>
|
||||
{/* No merchant-settings API yet — these operational controls cannot be persisted live. */}
|
||||
<AwaitingApi label="Merchant settings persistence" api="[R6]" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'payment' && (
|
||||
<div className="bg-white border border-slate-200/60 p-6 rounded-2xl shadow-sm space-y-lg animate-in fade-in duration-200">
|
||||
{/* Group 1: Checkout Gateways */}
|
||||
<div>
|
||||
<span className="text-xs font-bold text-slate-450 uppercase tracking-widest block">Payment & Tax</span>
|
||||
<h2 className="text-xl font-bold text-slate-900 mt-1">Checkout & Taxation</h2>
|
||||
</div>
|
||||
|
||||
{/* Live (read-only) tenant payment details. */}
|
||||
<div className="space-y-sm">
|
||||
<span className="text-xs font-bold text-purple-800 bg-purple-50/70 border border-purple-100/50 px-3 py-1 rounded-full uppercase tracking-wider">
|
||||
Checkout Gateways
|
||||
Store Payment Details
|
||||
</span>
|
||||
<div className="divide-y divide-slate-100/70 mt-2">
|
||||
<Row title="Cash on Delivery" desc="Allow customers to pay on delivery.">
|
||||
<Toggle checked={form.codEnabled} onChange={() => set('codEnabled', !form.codEnabled)} />
|
||||
<Row title="Minimum Order Value" desc="Smallest order a customer can place (from store profile).">
|
||||
<span className="font-bold text-slate-700 text-sm font-mono">
|
||||
{tenant && fnum(tenant.minorder) ? `₹${fnum(tenant.minorder).toLocaleString('en-IN')}` : '—'}
|
||||
</span>
|
||||
</Row>
|
||||
<Row title="Online Payments" desc="Accept UPI / card / wallet at checkout.">
|
||||
<Toggle checked={form.onlinePaymentEnabled} onChange={() => set('onlinePaymentEnabled', !form.onlinePaymentEnabled)} />
|
||||
<Row title="Payment Gateway ID" desc="Configured payment type for this store.">
|
||||
<span className="font-mono font-black bg-purple-100 px-3 py-1.5 rounded-xl border border-purple-200/40 text-xs">
|
||||
{tenant && fnum(tenant.paymenttype) ? fnum(tenant.paymenttype) : '—'}
|
||||
</span>
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Group 2: Taxation & Rules */}
|
||||
{/* Editable checkout gateways + tax rules have no persistence backend. */}
|
||||
<div className="space-y-sm">
|
||||
<span className="text-xs font-bold text-purple-800 bg-purple-50/70 border border-purple-100/50 px-3 py-1 rounded-full uppercase tracking-wider">
|
||||
Taxation & Cart Limits
|
||||
Checkout Gateways & Taxation
|
||||
</span>
|
||||
<div className="divide-y divide-slate-100/70 mt-2">
|
||||
<Row title="Default Tax Rate" desc="Applied to taxable catalogue items.">
|
||||
<div className="relative rounded-xl shadow-sm">
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
max={100}
|
||||
value={form.defaultTaxPercent}
|
||||
onChange={(e) => set('defaultTaxPercent', Number(e.target.value))}
|
||||
className="w-28 pr-7 pl-4 py-2 border border-slate-200 rounded-xl text-right font-bold text-slate-700 bg-slate-50/40 hover:bg-slate-50 focus:bg-white focus:outline-none focus:border-purple-500 focus:ring-2 focus:ring-purple-500/10 transition-all text-sm font-mono"
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
|
||||
<span className="text-slate-400 font-bold text-sm">%</span>
|
||||
</div>
|
||||
</div>
|
||||
</Row>
|
||||
<Row title="Minimum Order Value" desc="Smallest order a customer can place.">
|
||||
<div className="relative rounded-xl shadow-sm">
|
||||
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<span className="text-slate-400 font-bold text-sm">₹</span>
|
||||
</div>
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
value={form.minOrderValue}
|
||||
onChange={(e) => set('minOrderValue', Number(e.target.value))}
|
||||
className="w-28 pl-7 pr-4 py-2 border border-slate-200 rounded-xl text-right font-bold text-slate-700 bg-slate-50/40 hover:bg-slate-50 focus:bg-white focus:outline-none focus:border-purple-500 focus:ring-2 focus:ring-purple-500/10 transition-all text-sm font-mono"
|
||||
/>
|
||||
</div>
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* API synchronization details */}
|
||||
<div className="p-4 bg-purple-50/50 border border-purple-100/50 rounded-2xl text-purple-900 text-xs font-semibold flex items-center justify-between">
|
||||
<span className="text-slate-650 font-bold text-xs">Payment Gateway ID</span>
|
||||
<span className="font-mono font-black bg-purple-100 px-3 py-1.5 rounded-xl border border-purple-200/40 text-xs">{fnum(tenant?.paymenttype) || 'PAY-MOCK-99'}</span>
|
||||
<AwaitingApi className="mt-2" label="Merchant settings persistence" api="[R6]" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -681,92 +543,28 @@ export default function SettingsView({ tenantId = FIESTA_TENANT_ID }: SettingsVi
|
||||
className="border border-slate-200 bg-slate-50/40 hover:bg-slate-50 focus:bg-white rounded-xl py-2 px-3 font-bold text-slate-700 outline-none cursor-pointer focus:border-purple-500 transition-all text-sm shadow-sm"
|
||||
>
|
||||
{roleOptions.map((r) => (
|
||||
<option key={r} value={r}>{roleName(r)}</option>
|
||||
<option key={r.id} value={r.id}>{r.name}</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="border border-slate-200 bg-slate-50/40 hover:bg-slate-50 focus:bg-white rounded-xl py-2 px-3 font-bold text-slate-700 outline-none cursor-pointer focus:border-purple-500 transition-all text-sm shadow-sm"
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
<p className="text-[11px] text-slate-400 font-medium mt-2 px-4">
|
||||
Region and default-role are in-session workspace preferences applied at runtime; they are not saved to a backend.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Group 2: Notifications */}
|
||||
{/* Group 2: Notifications, sync interval & sandbox — no persistence backend. */}
|
||||
<div className="space-y-sm">
|
||||
<span className="text-xs font-bold text-purple-800 bg-purple-50/70 border border-purple-100/50 px-3 py-1 rounded-full uppercase tracking-wider">
|
||||
Notifications
|
||||
Notifications, Sync & Test Mode
|
||||
</span>
|
||||
<div className="divide-y divide-slate-100/70 mt-2">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Group 3: Test Mode (Sandbox) */}
|
||||
<div className="space-y-sm">
|
||||
<span className="text-xs font-bold text-purple-800 bg-purple-50/70 border border-purple-100/50 px-3 py-1 rounded-full uppercase tracking-wider">
|
||||
Test Mode (Sandbox)
|
||||
</span>
|
||||
<div className="divide-y divide-slate-100/70 mt-2">
|
||||
<Row title="Sandbox Mode" desc="Simulate warning states for testing without affecting live operations.">
|
||||
<Toggle checked={form.sandboxMode} onChange={() => set('sandboxMode', !form.sandboxMode)} />
|
||||
</Row>
|
||||
</div>
|
||||
<AwaitingApi className="mt-2" label="Merchant settings persistence" api="[R6]" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Floating Save Actions Bar (Frosted Glass) */}
|
||||
<div className={`fixed bottom-6 left-6 sm:left-[28%] right-6 bg-white/75 backdrop-blur-md border border-slate-200/80 rounded-2xl p-4 shadow-[0_20px_50px_rgba(0,0,0,0.12)] flex flex-col sm:flex-row sm:items-center justify-between gap-4 z-40 transition-all duration-500 ease-out transform select-none ${
|
||||
activeTab === 'users' ? 'hidden' :
|
||||
dirty ? 'translate-y-0 opacity-100' : 'translate-y-16 opacity-0 pointer-events-none'
|
||||
}`}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-2.5 h-2.5 rounded-full bg-amber-500 animate-pulse shrink-0" />
|
||||
<span className="text-sm font-bold text-slate-800">You have unsaved configuration changes</span>
|
||||
</div>
|
||||
<div className="flex gap-2.5">
|
||||
<button
|
||||
onClick={handleReset}
|
||||
className="px-4 py-2.5 border border-slate-200 bg-white/50 hover:bg-slate-100 rounded-xl text-xs font-bold text-slate-650 transition-all cursor-pointer flex items-center gap-1.5 active:scale-95 shadow-sm"
|
||||
>
|
||||
<RotateCcw size={14} /> Reset
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
className="px-4 py-2.5 bg-purple-650 hover:bg-purple-755 text-white rounded-xl text-xs font-bold transition-all cursor-pointer shadow-sm flex items-center gap-1.5 active:scale-95 border-none"
|
||||
>
|
||||
<Check size={14} /> 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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user