dispatch page
This commit is contained in:
@@ -14,19 +14,17 @@ import {
|
||||
MapPin,
|
||||
Phone,
|
||||
Mail,
|
||||
Plus
|
||||
Plus,
|
||||
Bike
|
||||
} 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';
|
||||
import AdminConsole from './AdminConsole';
|
||||
|
||||
interface SettingsViewProps {
|
||||
tenantId?: number;
|
||||
}
|
||||
|
||||
type TabKey = 'profile' | 'outlets' | 'users' | 'delivery' | 'payment' | 'preferences';
|
||||
type TabKey = 'profile' | 'outlets' | 'users';
|
||||
|
||||
/** Locally-persisted merchant preferences (survive reload via localStorage). */
|
||||
interface MerchantSettings {
|
||||
@@ -138,6 +136,13 @@ export default function SettingsView({ tenantId = FIESTA_TENANT_ID }: SettingsVi
|
||||
// (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 });
|
||||
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).
|
||||
@@ -177,9 +182,6 @@ export default function SettingsView({ tenantId = FIESTA_TENANT_ID }: SettingsVi
|
||||
{ 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 },
|
||||
];
|
||||
|
||||
// Build role options from the live app-roles API; fall back to the known
|
||||
@@ -392,74 +394,91 @@ export default function SettingsView({ tenantId = FIESTA_TENANT_ID }: SettingsVi
|
||||
<div className="bg-white border border-slate-200/60 p-6 rounded-2xl shadow-sm space-y-md animate-in fade-in duration-200">
|
||||
<div className="flex justify-between items-center pb-4 border-b border-slate-100">
|
||||
<div>
|
||||
<span className="text-xs font-bold text-slate-400 uppercase tracking-widest block">Our Stores</span>
|
||||
<h2 className="text-xl font-bold text-slate-900 mt-1">Store Directory</h2>
|
||||
<span className="text-xs font-bold text-slate-450 uppercase tracking-widest block">
|
||||
{showStoreOnboarding ? 'Onboarding' : 'Our Stores'}
|
||||
</span>
|
||||
<h2 className="text-xl font-bold text-slate-900 mt-1">
|
||||
{showStoreOnboarding ? 'Add Store Outlet Location' : 'Store Directory'}
|
||||
</h2>
|
||||
</div>
|
||||
<span className="text-xs text-[#581c87] font-bold bg-purple-50 px-3.5 py-1.5 rounded-full border border-purple-100/50">
|
||||
{locationsQ.isLoading ? 'Loading…' : `${cleanOutlets.length} outlet${cleanOutlets.length === 1 ? '' : 's'}`}
|
||||
</span>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowStoreOnboarding(!showStoreOnboarding)}
|
||||
className="bg-[#581c87] 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'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{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 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) => (
|
||||
<div key={loc.locationid || i} className="p-5 border border-slate-200/60 rounded-2xl bg-white hover:border-purple-300 hover:shadow-md transition-all duration-350 flex flex-col justify-between group">
|
||||
<div className="space-y-4">
|
||||
{/* Header: Outlet name & status */}
|
||||
<div className="flex justify-between items-start gap-3">
|
||||
<div className="flex gap-3">
|
||||
<div className="p-3 rounded-xl bg-purple-50 text-purple-650 shrink-0 self-start group-hover:bg-purple-100 transition-colors">
|
||||
<Store size={20} />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<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].filter(Boolean).join(', ') || '—'}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className={`shrink-0 px-2.5 py-1 rounded-full text-[9px] font-black uppercase tracking-wider ${
|
||||
loc.status.toLowerCase() === 'active'
|
||||
? 'text-emerald-700 bg-emerald-50 border border-emerald-100/50'
|
||||
: 'text-zinc-555 bg-zinc-100'
|
||||
}`}>
|
||||
{loc.status || '—'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Outlet Details Grid */}
|
||||
<div className="grid grid-cols-2 gap-3 bg-slate-50/50 p-3.5 rounded-xl border border-slate-100/80">
|
||||
<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">
|
||||
{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 ? `${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" />
|
||||
{loc.opentime && loc.closetime
|
||||
? `Open: ${formatFriendlyTime(loc.opentime)} – ${formatFriendlyTime(loc.closetime)}`
|
||||
: 'Hours not set'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{showStoreOnboarding ? (
|
||||
<div className="pt-2">
|
||||
<AdminConsole activeTab="store" showHeader={false} onBack={() => setShowStoreOnboarding(false)} tenantId={tenantId} />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{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 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) => (
|
||||
<div key={loc.locationid || i} className="p-5 border border-slate-200/60 rounded-2xl bg-white hover:border-purple-300 hover:shadow-md transition-all duration-350 flex flex-col justify-between group">
|
||||
<div className="space-y-4">
|
||||
{/* Header: Outlet name & status */}
|
||||
<div className="flex justify-between items-start gap-3">
|
||||
<div className="flex gap-3">
|
||||
<div className="p-3 rounded-xl bg-purple-50 text-purple-650 shrink-0 self-start group-hover:bg-purple-100 transition-colors">
|
||||
<Store size={20} />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<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].filter(Boolean).join(', ') || '—'}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className={`shrink-0 px-2.5 py-1 rounded-full text-[9px] font-black uppercase tracking-wider ${
|
||||
loc.status.toLowerCase() === 'active'
|
||||
? 'text-emerald-700 bg-emerald-50 border border-emerald-100/50'
|
||||
: 'text-zinc-555 bg-zinc-100'
|
||||
}`}>
|
||||
{loc.status || '—'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Outlet Details Grid */}
|
||||
<div className="grid grid-cols-2 gap-3 bg-slate-50/50 p-3.5 rounded-xl border border-slate-100/80">
|
||||
<div className="space-y-1">
|
||||
<span className="text-[10px] text-slate-455 uppercase font-bold block">Delivery Range</span>
|
||||
<p className="font-bold text-slate-700 text-xs">
|
||||
{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 ? `${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" />
|
||||
{loc.opentime && loc.closetime
|
||||
? `Open: ${formatFriendlyTime(loc.opentime)} – ${formatFriendlyTime(loc.closetime)}`
|
||||
: 'Hours not set'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -468,100 +487,7 @@ export default function SettingsView({ tenantId = FIESTA_TENANT_ID }: SettingsVi
|
||||
<UsersPanel tenantId={tenantId} defaultNewUserRole={form.defaultNewUserRole} />
|
||||
)}
|
||||
|
||||
{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">
|
||||
<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">
|
||||
<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">
|
||||
Store Payment Details
|
||||
</span>
|
||||
<div className="divide-y divide-slate-100/70 mt-2">
|
||||
<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="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>
|
||||
|
||||
{/* 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">
|
||||
Checkout Gateways & Taxation
|
||||
</span>
|
||||
<AwaitingApi className="mt-2" label="Merchant settings persistence" api="[R6]" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'preferences' && (
|
||||
<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: General Defaults */}
|
||||
<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">
|
||||
General Defaults
|
||||
</span>
|
||||
<div className="divide-y divide-slate-100/70 mt-2">
|
||||
<Row title="Default Region" desc="Region applied to new outlets and reports.">
|
||||
<div className="relative rounded-xl shadow-sm">
|
||||
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<MapPin size={14} className="text-slate-400" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={form.defaultRegion}
|
||||
onChange={(e) => set('defaultRegion', e.target.value)}
|
||||
className="w-44 pl-8 pr-4 py-2 border border-slate-200 rounded-xl font-bold text-slate-700 bg-slate-50/40 hover:bg-slate-50 focus:bg-white outline-none focus:border-purple-500 focus:ring-2 focus:ring-purple-500/10 transition-all text-sm text-right"
|
||||
/>
|
||||
</div>
|
||||
</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="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.id} value={r.id}>{r.name}</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, 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, Sync & Test Mode
|
||||
</span>
|
||||
<AwaitingApi className="mt-2" label="Merchant settings persistence" api="[R6]" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user