/** * @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, Plus, Bike } from 'lucide-react'; import { useFiestaAllTenants, useFiestaTenantLocations, useFiestaUpdateLocation, useFiestaDeleteLocation } 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'; import AdminConsole from './AdminConsole'; import { ConfirmModal } from './ConfirmModal'; import type { AuthUser } from '../services/auth'; type TabKey = 'profile' | 'outlets' | 'users'; /** 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 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, }; const formatFriendlyTime = (timeStr: string) => { try { if (timeStr.includes('T')) { const parts = timeStr.split('T')[1].split(':'); let hour = parseInt(parts[0], 10); const min = parts[1]; const ampm = hour >= 12 ? 'PM' : 'AM'; hour = hour % 12; hour = hour ? hour : 12; return `${hour}:${min} ${ampm}`; } if (timeStr.includes(':')) { const parts = timeStr.split(':'); let hour = parseInt(parts[0], 10); const min = parts[1].slice(0, 2); const ampm = hour >= 12 ? 'PM' : 'AM'; hour = hour % 12; hour = hour ? hour : 12; return `${hour}:${min} ${ampm}`; } } catch { // fallback } return timeStr; }; /// ── Small presentational helpers ──────────────────────────────────────────── function Row({ title, desc, children, }: { title: string; desc?: string; children: React.ReactNode; }) { return (

{title}

{desc &&

{desc}

}
{children}
); } export interface SettingsViewProps { tenantId?: number; user?: AuthUser; } export default function SettingsView({ tenantId = FIESTA_TENANT_ID, user }: SettingsViewProps) { const [activeTab, setActiveTab] = useState('profile'); // Live tenant profile + outlets. // Fetch a larger page size to ensure we find our specific tenant (ID 1087). const tenantsQ = useFiestaAllTenants({ pagesize: 5000, status: '' }); const tenant = (tenantsQ.data ?? []).find((t) => Number(t.tenantid) === tenantId) || null; const locationsQ = useFiestaTenantLocations(tenantId); const outlets = locationsQ.data ?? []; // Application roles (Hasura) — drives the role dropdowns. const rolesQ = useAppRoles(); // 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({ ...DEFAULTS }); const updateLocationMut = useFiestaUpdateLocation(); const deleteLocationMut = useFiestaDeleteLocation(); const [editingStore, setEditingStore] = useState(null); const [showEditStoreModal, setShowEditStoreModal] = useState(false); const [confirmModalState, setConfirmModalState] = useState<{isOpen: boolean, loc: any}>({ isOpen: false, loc: null }); const handleUpdateStoreSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!editingStore) return; try { await updateLocationMut.mutateAsync({ tenantid: Number(tenantId), locationid: Number(editingStore.locationid), locationname: editingStore.locationname, suburb: editingStore.suburb, city: editingStore.city, contactno: editingStore.contactno, status: editingStore.status, }); setShowEditStoreModal(false); setEditingStore(null); alert('Outlet updated successfully!'); } catch (err: any) { alert(err.message || 'Failed to update outlet.'); } }; const handleDeleteStore = async (loc: any) => { setConfirmModalState({ isOpen: true, loc }); }; const confirmDeleteOutlet = async () => { const loc = confirmModalState.loc; if (!loc) return; try { await deleteLocationMut.mutateAsync({ tenantid: tenantId, locationid: loc.locationid }); setConfirmModalState({ isOpen: false, loc: null }); } catch (err: any) { alert(err.message || 'Failed to delete outlet.'); setConfirmModalState({ isOpen: false, loc: null }); } }; const [showStoreOnboarding, setShowStoreOnboarding] = useState(false); useEffect(() => { if (activeTab !== 'outlets') { setShowStoreOnboarding(false); } }, [activeTab]); // 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; 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', })); }, [tenant]); // Live outlets only — no fabricated fallback. Render whatever the API returns. const cleanOutlets = useMemo(() => { 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 set = (key: K, value: MerchantSettings[K]) => setForm((f) => ({ ...f, [key]: value })); 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 }, ]; // 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>(() => { const rows = rolesQ.data ?? []; const mapped = rows .map((r) => { const id = fnum((r as Record).roleid); const name = fstr((r as Record).rolename) || fstr((r as Record).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 (
{/* Header Removed */}
{/* Tab rail & Merchant Card */}
{/* Merchant ID Card */}
{/* Background design accents */}
{/* Initials avatar badge with glowing ring */}
{user?.name ? user.name.substring(0, 2).toUpperCase() : tenant ? fstr(tenant.tenantname).substring(0, 2).toUpperCase() : 'ND'}

{user?.name || (tenant ? fstr(tenant.tenantname) : 'Nearle Merchant')}

Store ID: #{tenantId}

Status Online & Synced
{/* Navigation tab rail */}
{/* Panel */}
{activeTab === 'profile' && (
Store Profile

Identity & Contacts

{/* Identity Row */}
{user?.name || fstr(tenant?.tenantname) || 'Nearle Store'} — View your customer service email and contact number. Official registration details are synced with your primary credentials.
{/* Live identity (read-only) */}
Official Registration Info
Company Name

{fstr(tenant?.companyname) || user?.name || 'Nearle Merchant'}

Category

{fstr(tenant?.subcategoryname) || (tenant?.categoryid ? `Category ${fnum(tenant.categoryid)}` : 'General Retail')}

Registration Status

{fstr(tenant?.status) || (user ? 'Active' : '—')}

Store Verification

{fnum(tenant?.approved) === 1 || user ? 'Verified' : 'Pending'}

{fnum(tenant?.approved) === 1 || user ? 'Verified' : 'Pending'}
Registered Address

{fstr(tenant?.address) || [user?.locationname, user?.applocation].filter(Boolean).join(', ') || 'Headquarters'} {tenant?.city ? ` · ${fstr(tenant.city)}, ${fstr(tenant.state)} ${fstr(tenant.postcode)}` : ''}

{/* Customer support contacts — live (read-only) tenant values. */}
Customer Support & Contacts
)} {activeTab === 'outlets' && (
{showStoreOnboarding ? 'Onboarding' : 'Our Stores'}

{showStoreOnboarding ? 'Add Store Outlet Location' : 'Store Directory'}

{showStoreOnboarding ? (
setShowStoreOnboarding(false)} tenantId={tenantId} />
) : ( <> {locationsQ.isLoading ? (
Loading live outlets…
) : cleanOutlets.length === 0 ? (
No outlets configured yet.
) : (
{cleanOutlets.map((loc, i) => (
{/* Header: Outlet name & status */}

{loc.locationname}

{[loc.suburb, loc.city].filter(Boolean).join(', ') || '—'}

{loc.status || '—'}
{/* Outlet Details Grid */}
Delivery Range

{loc.deliveryradius ? `Up to ${loc.deliveryradius / 1000} km` : '—'}

Opening Hours

{loc.opentime && loc.closetime ? `Open: ${formatFriendlyTime(loc.opentime)} – ${formatFriendlyTime(loc.closetime)}` : 'Hours not set'}

))}
)} )}
)} {activeTab === 'users' && ( )}
{/* EDIT STORE MODAL */} {showEditStoreModal && editingStore && (
{ if (e.target === e.currentTarget) setShowEditStoreModal(false); }} >
e.stopPropagation()}>

Edit Store Outlet Location

Update Outlet Profile

Adjust the operational metadata for this node. The outlet will instantly be updated across the delivery fleet system.

setEditingStore({ ...editingStore, locationname: e.target.value })} className="w-full border border-[#e2e8f0] rounded-lg p-sm bg-[#f8fafc] focus:bg-white outline-none focus:ring-1 focus:ring-[#662582] text-xs" required />
setEditingStore({ ...editingStore, suburb: e.target.value })} className="w-full border border-[#e2e8f0] rounded-lg p-sm bg-[#f8fafc] focus:bg-white outline-none focus:ring-1 focus:ring-[#662582] text-xs" required />
setEditingStore({ ...editingStore, city: e.target.value })} className="w-full border border-[#e2e8f0] rounded-lg p-sm bg-[#f8fafc] focus:bg-white outline-none focus:ring-1 focus:ring-[#662582] text-xs" />
setEditingStore({ ...editingStore, contactno: e.target.value })} className="w-full border border-[#e2e8f0] rounded-lg p-sm bg-[#f8fafc] focus:bg-white outline-none focus:ring-1 focus:ring-[#662582] text-xs" required />
)} Are you sure you want to delete {confirmModalState.loc?.locationname}? This action cannot be undone. } confirmText="Delete Outlet" onConfirm={confirmDeleteOutlet} onCancel={() => setConfirmModalState({ isOpen: false, loc: null })} isDestructive={true} isLoading={deleteLocationMut.isPending} />
); }