/**
* @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 (
);
}
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 */}
{tabs.map((t) => {
const Icon = t.icon;
const active = activeTab === t.key;
return (
setActiveTab(t.key)}
className={`flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-bold transition-all duration-200 whitespace-nowrap cursor-pointer border-none ${
active
? 'bg-purple-50 text-[#662582] shadow-sm border-l-2 border-purple-650'
: 'text-slate-600 hover:text-slate-900 hover:bg-white bg-transparent'
}`}
>
{t.label}
);
})}
{/* 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'}
setShowStoreOnboarding(!showStoreOnboarding)}
className="bg-[#662582] hover:bg-purple-800 text-white px-4 py-2.5 rounded-xl text-xs font-bold uppercase tracking-wider flex items-center gap-1.5 cursor-pointer shadow-sm active:scale-95 transition-all border-none"
>
{showStoreOnboarding ? 'View Store Directory' : '+ Add Store Branch'}
{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 || '—'}
{
setEditingStore({ ...loc });
setShowEditStoreModal(true);
}}
className="text-[10px] uppercase font-bold text-purple-600 hover:text-purple-800 hover:bg-purple-50 px-2 py-1 rounded border border-purple-200 transition-colors"
>
Edit
handleDeleteStore(loc)}
className="text-[10px] uppercase font-bold text-red-600 hover:text-red-800 hover:bg-red-50 px-2 py-1 rounded border border-red-200 transition-colors"
>
Delete
{/* 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
setShowEditStoreModal(false)}
className="text-zinc-400 hover:text-zinc-700 bg-zinc-100 hover:bg-zinc-200 p-1.5 rounded-lg transition-colors cursor-pointer border-none"
>
✕
)}
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}
/>
);
}