dispatch page

This commit is contained in:
Gokul
2026-06-12 14:45:06 +05:30
parent d8c1517239
commit 5378f2df1f
34 changed files with 4451 additions and 1744 deletions

View File

@@ -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>