Add Super Admin login routing and wire tenant onboarding to auto-create a location

super_admin is now a real LoginRole, derived from the server's
issuperadmin flag (not a client-guessable roleid) — routes exclusively
to a new minimal SuperAdminPage, never the merchant console.

That page reuses the existing tenant/store/rider onboarding wizard
(AdminConsole) rather than duplicating it — its Tenant tab was already
calling the right composite endpoint but had nowhere to send location
data, and was never actually reachable from anywhere in the app. Now
it collects a primary outlet name + business category and sends a
nested tenantlocations object, so one submit provisions tenant +
active location + admin user instead of leaving the tenant
locationless. createTenantUser also moves off the mob API base onto
the web one, matching where this is actually called from.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Suriya
2026-07-16 20:46:07 +05:30
parent 186546f1fe
commit ecbae9d8fc
5 changed files with 143 additions and 9 deletions

View File

@@ -51,7 +51,7 @@ export default function AdminConsole({ activeTab: propActiveTab, showHeader = tr
// ----------------------------------------------------
// Form State: Tenant Onboarding
// ----------------------------------------------------
const [tenantForm, setTenantForm] = useState({
const TENANT_FORM_INITIAL = {
tenantname: '',
companyname: '',
primarycontact: '',
@@ -61,7 +61,15 @@ export default function AdminConsole({ activeTab: propActiveTab, showHeader = tr
city: 'Coimbatore',
state: 'Tamil Nadu',
postcode: '',
});
// Primary outlet — created in the same call as the tenant, so a fresh
// tenant is never left without a location to actually operate from.
locationname: '',
// 1 Food & Dining, 2 Grocery & Daily, 3 Pharmacy, 4 Retail — the same
// categories categoryName() in fiestaMappers.ts already displays.
categoryid: 2,
applocationid: 1,
};
const [tenantForm, setTenantForm] = useState(TENANT_FORM_INITIAL);
const [tenantSuccess, setTenantSuccess] = useState<any>(null);
// ----------------------------------------------------
@@ -124,7 +132,7 @@ export default function AdminConsole({ activeTab: propActiveTab, showHeader = tr
// ----------------------------------------------------
const handleTenantSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!tenantForm.tenantname || !tenantForm.companyname || !tenantForm.primarycontact || !tenantForm.primaryemail) {
if (!tenantForm.tenantname || !tenantForm.companyname || !tenantForm.primarycontact || !tenantForm.primaryemail || !tenantForm.locationname) {
alert('Kindly fill in all required fields.');
return;
}
@@ -133,6 +141,20 @@ export default function AdminConsole({ activeTab: propActiveTab, showHeader = tr
...tenantForm,
approved: 1,
status: 'Active',
// Reuses the tenant's own address for its primary location — a
// freshly onboarded single-location merchant's outlet address is
// the same as their registered one.
tenantlocations: {
locationname: tenantForm.locationname,
email: tenantForm.primaryemail,
contactno: tenantForm.primarycontact,
address: tenantForm.address,
suburb: tenantForm.suburb,
city: tenantForm.city,
state: tenantForm.state,
postcode: tenantForm.postcode,
applocationid: tenantForm.applocationid,
},
});
setTenantSuccess(res);
alert('Tenant Onboarded successfully!');
@@ -244,7 +266,7 @@ VALUES (${newUserId}, 1, 'Active', NOW());
</p>
<button
type="button"
onClick={() => { setTenantSuccess(null); setTenantForm({ tenantname: '', companyname: '', primarycontact: '', primaryemail: '', address: '', suburb: '', city: 'Coimbatore', state: 'Tamil Nadu', postcode: '' }); }}
onClick={() => { setTenantSuccess(null); setTenantForm(TENANT_FORM_INITIAL); }}
className="bg-emerald-600 hover:bg-emerald-700 text-white font-bold text-xs uppercase tracking-wider px-5 py-2.5 rounded-lg border-none cursor-pointer"
>
Onboard Another Tenant
@@ -300,6 +322,33 @@ VALUES (${newUserId}, 1, 'Active', NOW());
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-1">
<label className="text-[10px] font-bold text-slate-450 uppercase tracking-widest">Primary Outlet Name (*)</label>
<input
type="text"
placeholder="e.g. Kaveri Groceries - Main Branch"
value={tenantForm.locationname}
onChange={(e) => setTenantForm({ ...tenantForm, locationname: e.target.value })}
className="w-full border border-slate-250 rounded-xl p-3 bg-slate-50/40 hover:bg-slate-100 focus:bg-white outline-none focus:border-purple-500 transition-all font-semibold text-xs text-slate-800"
required
/>
</div>
<div className="space-y-1">
<label className="text-[10px] font-bold text-slate-450 uppercase tracking-widest">Business Category</label>
<select
value={tenantForm.categoryid}
onChange={(e) => setTenantForm({ ...tenantForm, categoryid: Number(e.target.value) })}
className="w-full border border-slate-250 rounded-xl p-3 bg-slate-50/40 hover:bg-slate-100 focus:bg-white outline-none focus:border-purple-500 transition-all font-semibold text-xs text-slate-800 cursor-pointer"
>
<option value={1}>Food & Dining</option>
<option value={2}>Grocery & Daily</option>
<option value={3}>Pharmacy</option>
<option value={4}>Retail</option>
</select>
</div>
</div>
<div className="space-y-1">
<label className="text-[10px] font-bold text-slate-450 uppercase tracking-widest">HQ Street Address</label>
<input
@@ -972,7 +1021,7 @@ VALUES (${newUserId}, 1, 'Active', NOW());
Tenant <strong>{tenantForm.tenantname}</strong> has been provisioned. An administrator account has been dispatched with credentials tied to <strong>{tenantForm.primaryemail}</strong>.
</p>
<button
onClick={() => { setTenantSuccess(null); setTenantForm({ tenantname: '', companyname: '', primarycontact: '', primaryemail: '', address: '', suburb: '', city: 'Coimbatore', state: 'Tamil Nadu', postcode: '' }); }}
onClick={() => { setTenantSuccess(null); setTenantForm(TENANT_FORM_INITIAL); }}
className="bg-emerald-600 hover:bg-emerald-700 text-white font-bold text-xs uppercase tracking-wider px-5 py-2.5 rounded-lg border-none cursor-pointer"
>
Onboard Another Tenant