AdminConsole renders the Tenant Onboarding form twice (embedded vs. showHeader standalone) from two separate JSX blocks. The Primary Outlet Name / Business Category fields added earlier only landed in the embedded block, so the showHeader block SuperAdminPage actually renders had no way to fill tenantForm.locationname — the required- field check on submit failed unconditionally, no matter what was filled in. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1624 lines
92 KiB
TypeScript
1624 lines
92 KiB
TypeScript
/**
|
|
* @license
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
Building2,
|
|
Store,
|
|
Bike,
|
|
Check,
|
|
Copy,
|
|
Plus,
|
|
ArrowRight,
|
|
ArrowLeft,
|
|
ShieldAlert,
|
|
Loader2,
|
|
Mail,
|
|
Phone,
|
|
FileText,
|
|
MapPin,
|
|
Clock,
|
|
Navigation
|
|
} from 'lucide-react';
|
|
import {
|
|
useFiestaCreateTenant,
|
|
useFiestaCreateLocation,
|
|
useFiestaCreateUser,
|
|
useFiestaRiderShifts,
|
|
useFiestaTenantLocations,
|
|
useFiestaAllTenants
|
|
} from '../services/fiestaQueries';
|
|
import { FIESTA_TENANT_ID, str as fstr, num as fnum } from '../services/fiestaApi';
|
|
|
|
export default function AdminConsole({ activeTab: propActiveTab, showHeader = true, onBack, tenantId }: { activeTab?: 'tenant' | 'store' | 'rider', showHeader?: boolean, onBack?: () => void, tenantId?: number }) {
|
|
const [activeTab, setActiveTab] = useState<'tenant' | 'store' | 'rider'>(propActiveTab || 'tenant');
|
|
|
|
useEffect(() => {
|
|
if (propActiveTab) {
|
|
setActiveTab(propActiveTab);
|
|
}
|
|
}, [propActiveTab]);
|
|
|
|
// Mutations & Queries
|
|
const createTenantMut = useFiestaCreateTenant();
|
|
const createLocationMut = useFiestaCreateLocation();
|
|
const createUserMut = useFiestaCreateUser();
|
|
const shiftsQ = useFiestaRiderShifts();
|
|
const tenantsQ = useFiestaAllTenants({ pagesize: 100 });
|
|
|
|
// ----------------------------------------------------
|
|
// Form State: Tenant Onboarding
|
|
// ----------------------------------------------------
|
|
const TENANT_FORM_INITIAL = {
|
|
tenantname: '',
|
|
companyname: '',
|
|
primarycontact: '',
|
|
primaryemail: '',
|
|
address: '',
|
|
suburb: '',
|
|
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);
|
|
|
|
// ----------------------------------------------------
|
|
// Form State: Store Onboarding
|
|
// ----------------------------------------------------
|
|
const [storeForm, setStoreForm] = useState({
|
|
tenantid: tenantId || FIESTA_TENANT_ID,
|
|
locationname: '',
|
|
address: '',
|
|
suburb: '',
|
|
city: 'Coimbatore',
|
|
state: 'Tamil Nadu',
|
|
postcode: '',
|
|
contactno: '',
|
|
email: '',
|
|
opentime: '06:00:00',
|
|
closetime: '22:00:00',
|
|
deliverymins: 45,
|
|
deliveryradius: 5000, // in meters
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (tenantId) {
|
|
setStoreForm(prev => ({ ...prev, tenantid: tenantId }));
|
|
}
|
|
}, [tenantId]);
|
|
|
|
const [storeSuccess, setStoreSuccess] = useState<any>(null);
|
|
|
|
const currentTenantObj = (tenantsQ.data || []).find((t) => Number(t.tenantid) === Number(storeForm.tenantid));
|
|
const tenantNameDisplay = currentTenantObj ? `${fstr(currentTenantObj.tenantname)} (#${fstr(currentTenantObj.tenantid)})` : `Tenant #${storeForm.tenantid}`;
|
|
|
|
// ----------------------------------------------------
|
|
// Form State: Rider Onboarding
|
|
// ----------------------------------------------------
|
|
const [riderForm, setRiderForm] = useState({
|
|
firstname: '',
|
|
lastname: '',
|
|
email: '',
|
|
contactno: '',
|
|
password: 'Rider@123',
|
|
locationid: 0,
|
|
shiftid: 0,
|
|
vehiclename: '',
|
|
vehiclemodel: '',
|
|
licensenumber: '',
|
|
});
|
|
const [riderSuccess, setRiderSuccess] = useState<any>(null);
|
|
const [copiedSql, setCopiedSql] = useState(false);
|
|
|
|
const handleCopySql = () => {
|
|
if (!riderSuccess?.sql) return;
|
|
navigator.clipboard.writeText(riderSuccess.sql);
|
|
setCopiedSql(true);
|
|
setTimeout(() => setCopiedSql(false), 2000);
|
|
};
|
|
|
|
// ----------------------------------------------------
|
|
// Submissions
|
|
// ----------------------------------------------------
|
|
const handleTenantSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!tenantForm.tenantname || !tenantForm.companyname || !tenantForm.primarycontact || !tenantForm.primaryemail || !tenantForm.locationname) {
|
|
alert('Kindly fill in all required fields.');
|
|
return;
|
|
}
|
|
try {
|
|
const res = await createTenantMut.mutateAsync({
|
|
...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!');
|
|
} catch (err: any) {
|
|
alert(err.message || 'Failed to onboard tenant.');
|
|
}
|
|
};
|
|
|
|
const handleStoreSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!storeForm.locationname) {
|
|
alert('Location Name is required.');
|
|
return;
|
|
}
|
|
try {
|
|
const res = await createLocationMut.mutateAsync({
|
|
...storeForm,
|
|
status: 'Active',
|
|
moduleid: 2,
|
|
applocationid: 1,
|
|
partnerid: 1,
|
|
roleid: 4,
|
|
});
|
|
setStoreSuccess(res);
|
|
alert('Store Location created successfully!');
|
|
} catch (err: any) {
|
|
alert(err.message || 'Failed to create store location.');
|
|
}
|
|
};
|
|
|
|
const handleRiderSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!riderForm.firstname || !riderForm.email || !riderForm.contactno || !riderForm.locationid) {
|
|
alert('Please fill out the Rider personal details and assign a store.');
|
|
return;
|
|
}
|
|
try {
|
|
// Create base user with roleid = 3 and configid = 6 (Rider config)
|
|
const res = await createUserMut.mutateAsync({
|
|
firstname: riderForm.firstname,
|
|
lastname: riderForm.lastname,
|
|
email: riderForm.email,
|
|
contactno: riderForm.contactno,
|
|
password: riderForm.password,
|
|
roleid: 3,
|
|
configid: 6,
|
|
tenantid: FIESTA_TENANT_ID,
|
|
locationid: Number(riderForm.locationid),
|
|
applocationid: 1,
|
|
status: 'active',
|
|
});
|
|
|
|
const newUserId = res.userid || Math.floor(Math.random() * 9000) + 1000;
|
|
const vehicleName = riderForm.vehiclename || 'Standard delivery bike';
|
|
const licenseNum = riderForm.licensenumber || 'TN-37-XX-XXXX';
|
|
const vehicleModel = riderForm.vehiclemodel || 'Electric Scooter';
|
|
const shiftId = Number(riderForm.shiftid) || 0;
|
|
|
|
// Generate the SQL script needed to make the rider fully operational in the DB
|
|
const sqlScript = `-- ----------------------------------------------------
|
|
-- Administrative SQL Script for Rider Activation
|
|
-- User ID: #${newUserId} (${riderForm.firstname})
|
|
-- ----------------------------------------------------
|
|
|
|
-- 1. Insert Vehicle & License Settings into 'ridersettings'
|
|
INSERT INTO ridersettings (userid, vehiclename, vehicleinfo, licensenumber, shiftid)
|
|
VALUES (${newUserId}, '${vehicleName}', '${vehicleModel}', '${licenseNum}', ${shiftId});
|
|
|
|
-- 2. Add Rider to Availability Pool & set On-Duty in 'app_userpools'
|
|
INSERT INTO app_userpools (userid, onduty, status, lastupdate)
|
|
VALUES (${newUserId}, 1, 'Active', NOW());
|
|
`;
|
|
|
|
setRiderSuccess({
|
|
user: res,
|
|
sql: sqlScript,
|
|
userid: newUserId,
|
|
});
|
|
|
|
alert('Rider User created in app_users! SQL Script generated below.');
|
|
} catch (err: any) {
|
|
alert(err.message || 'Failed to create rider user.');
|
|
}
|
|
};
|
|
|
|
// Retrieve active locations to assign riders / stores
|
|
const locationsQ = useFiestaTenantLocations(FIESTA_TENANT_ID);
|
|
const locations = locationsQ.data || [];
|
|
|
|
if (!showHeader) {
|
|
return (
|
|
<div className="space-y-gutter w-full">
|
|
{/* TAB 1: Tenant Onboarding */}
|
|
{activeTab === 'tenant' && (
|
|
<div className="bg-white border border-slate-200/60 p-6 rounded-2xl shadow-sm space-y-6">
|
|
<div>
|
|
<h3 className="text-lg font-bold text-slate-900">Provision New Merchant Tenant</h3>
|
|
<p className="text-slate-500 text-xs mt-1">
|
|
Onboard a new merchant group. This registers their enterprise, defaults the order serialization, and spawns the primary Administrator account.
|
|
</p>
|
|
</div>
|
|
|
|
{tenantSuccess ? (
|
|
<div className="bg-emerald-50 border border-emerald-100 rounded-2xl p-6 text-center space-y-4">
|
|
<div className="w-12 h-12 rounded-full bg-emerald-550 text-white flex items-center justify-center mx-auto text-xl font-bold">✓</div>
|
|
<h4 className="font-bold text-emerald-900 text-base">Onboarding Complete!</h4>
|
|
<p className="text-xs text-emerald-700 max-w-md mx-auto">
|
|
Tenant <strong>{tenantForm.tenantname}</strong> has been provisioned. An administrator account has been dispatched with credentials tied to <strong>{tenantForm.primaryemail}</strong>.
|
|
</p>
|
|
<button
|
|
type="button"
|
|
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
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<form onSubmit={handleTenantSubmit} className="space-y-4">
|
|
<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">Merchant Name (*)</label>
|
|
<input
|
|
type="text"
|
|
placeholder="e.g. Kaveri Groceries"
|
|
value={tenantForm.tenantname}
|
|
onChange={(e) => setTenantForm({ ...tenantForm, tenantname: 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">Company Registered Name (*)</label>
|
|
<input
|
|
type="text"
|
|
placeholder="e.g. Kaveri Retail Pvt. Ltd."
|
|
value={tenantForm.companyname}
|
|
onChange={(e) => setTenantForm({ ...tenantForm, companyname: 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">Primary Phone Number (*)</label>
|
|
<input
|
|
type="text"
|
|
maxLength={10}
|
|
placeholder="e.g. 9876543210"
|
|
value={tenantForm.primarycontact}
|
|
onChange={(e) => setTenantForm({ ...tenantForm, primarycontact: e.target.value.replace(/\D/g, '') })}
|
|
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">Primary Admin Email (*)</label>
|
|
<input
|
|
type="email"
|
|
placeholder="e.g. admin@kaveri.com"
|
|
value={tenantForm.primaryemail}
|
|
onChange={(e) => setTenantForm({ ...tenantForm, primaryemail: 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>
|
|
|
|
<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
|
|
type="text"
|
|
placeholder="e.g. 12, Avinashi Road"
|
|
value={tenantForm.address}
|
|
onChange={(e) => setTenantForm({ ...tenantForm, address: 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"
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] font-bold text-slate-450 uppercase tracking-widest">Suburb</label>
|
|
<input
|
|
type="text"
|
|
placeholder="e.g. Peelamedu"
|
|
value={tenantForm.suburb}
|
|
onChange={(e) => setTenantForm({ ...tenantForm, suburb: 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"
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] font-bold text-slate-450 uppercase tracking-widest">City</label>
|
|
<input
|
|
type="text"
|
|
value={tenantForm.city}
|
|
onChange={(e) => setTenantForm({ ...tenantForm, city: 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"
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] font-bold text-slate-450 uppercase tracking-widest">State</label>
|
|
<input
|
|
type="text"
|
|
value={tenantForm.state}
|
|
onChange={(e) => setTenantForm({ ...tenantForm, state: 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"
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] font-bold text-slate-450 uppercase tracking-widest">Postcode</label>
|
|
<input
|
|
type="text"
|
|
placeholder="e.g. 641004"
|
|
value={tenantForm.postcode}
|
|
onChange={(e) => setTenantForm({ ...tenantForm, postcode: 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"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-end pt-4">
|
|
<button
|
|
type="submit"
|
|
disabled={createTenantMut.isPending}
|
|
className="bg-purple-650 hover:bg-purple-755 text-white px-6 py-3 rounded-xl text-xs font-bold uppercase tracking-wider flex items-center gap-2 cursor-pointer shadow-sm active:scale-95 transition-all border-none"
|
|
>
|
|
{createTenantMut.isPending ? (
|
|
<>
|
|
<Loader2 size={14} className="animate-spin" />
|
|
Provisioning Tenant...
|
|
</>
|
|
) : (
|
|
<>
|
|
<Building2 size={14} />
|
|
Provision Tenant
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* TAB 2: Store Onboarding */}
|
|
{activeTab === 'store' && (
|
|
<div className="bg-slate-50/30 border border-slate-200/60 p-7 rounded-2xl shadow-sm space-y-6 animate-in fade-in duration-300">
|
|
<div className="flex justify-between items-start border-b border-slate-100 pb-5">
|
|
<div>
|
|
<h3 className="text-lg font-bold text-slate-900 flex items-center gap-2">
|
|
<Store className="text-[#662582]" size={20} />
|
|
Add Store Outlet Location
|
|
</h3>
|
|
<p className="text-slate-550 text-xs mt-1">
|
|
Commission a new store branch/hub under a tenant. This sets up store parameters, delivery thresholds, and spawns a placeholder branch manager account.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{storeSuccess ? (
|
|
<div className="bg-emerald-50 border border-emerald-100 rounded-2xl p-6 text-center space-y-4 shadow-sm animate-in zoom-in-95 duration-200">
|
|
<div className="w-12 h-12 rounded-full bg-emerald-500 text-white flex items-center justify-center mx-auto text-xl font-bold">✓</div>
|
|
<h4 className="font-bold text-emerald-900 text-base">Store Branch Active!</h4>
|
|
<p className="text-xs text-emerald-700 max-w-md mx-auto">
|
|
Store <strong>{storeForm.locationname}</strong> has been initialized. A default placeholder manager user has been spawned in inactive mode (requires password setup).
|
|
</p>
|
|
<div className="flex justify-center gap-3">
|
|
{onBack && (
|
|
<button
|
|
type="button"
|
|
onClick={onBack}
|
|
className="bg-white border border-slate-200 hover:bg-slate-50 text-slate-600 font-bold text-xs uppercase tracking-wider px-5 py-2.5 rounded-lg cursor-pointer flex items-center gap-1.5 shadow-sm active:scale-95 transition-all"
|
|
>
|
|
<ArrowLeft size={13} />
|
|
Back to Outlets
|
|
</button>
|
|
)}
|
|
<button
|
|
type="button"
|
|
onClick={() => { setStoreSuccess(null); setStoreForm({ tenantid: FIESTA_TENANT_ID, locationname: '', address: '', suburb: '', city: 'Coimbatore', state: 'Tamil Nadu', postcode: '', contactno: '', email: '', opentime: '06:00:00', closetime: '22:00:00', deliverymins: 45, deliveryradius: 5000 }); }}
|
|
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 active:scale-95 transition-all shadow-sm"
|
|
>
|
|
Add Another Branch
|
|
</button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<form onSubmit={handleStoreSubmit} className="space-y-6">
|
|
|
|
{/* SECTION 1: Identity & Primary Contact */}
|
|
<div className="bg-white border border-slate-200/60 p-5 rounded-2xl space-y-4 shadow-sm">
|
|
<h4 className="text-xs font-black text-purple-700 uppercase tracking-widest flex items-center gap-1.5 pb-2 border-b border-slate-100/60">
|
|
<Building2 size={13} /> Owner & Identity
|
|
</h4>
|
|
<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-500 uppercase tracking-widest block mb-1">Owner Tenant</label>
|
|
<div className="relative rounded-xl">
|
|
<div className="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none text-slate-400">
|
|
<Building2 size={14} />
|
|
</div>
|
|
<input
|
|
type="text"
|
|
value={tenantsQ.isLoading ? 'Loading tenant info...' : tenantNameDisplay}
|
|
readOnly
|
|
className="pl-10 pr-4 py-2.5 w-full border border-slate-200 rounded-xl bg-slate-100/75 outline-none font-semibold text-xs text-slate-500 shadow-sm cursor-not-allowed"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] font-bold text-slate-500 uppercase tracking-widest block mb-1">Store Outlet Name <span className="text-red-500">*</span></label>
|
|
<div className="relative rounded-xl">
|
|
<div className="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none text-slate-400">
|
|
<Store size={14} />
|
|
</div>
|
|
<input
|
|
type="text"
|
|
placeholder="e.g. RS Puram Super Hub"
|
|
value={storeForm.locationname}
|
|
onChange={(e) => setStoreForm({ ...storeForm, locationname: e.target.value })}
|
|
className="pl-10 pr-4 py-2.5 w-full border border-slate-200 rounded-xl bg-slate-50/40 hover:bg-slate-100/60 focus:bg-white outline-none focus:ring-4 focus:ring-purple-100 focus:border-purple-600 transition-all font-semibold text-xs text-slate-800 shadow-sm"
|
|
required
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] font-bold text-slate-500 uppercase tracking-widest block mb-1">Outlet Email Address</label>
|
|
<div className="relative rounded-xl">
|
|
<div className="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none text-slate-400">
|
|
<Mail size={14} />
|
|
</div>
|
|
<input
|
|
type="email"
|
|
placeholder="e.g. rspuram@kaveri.com"
|
|
value={storeForm.email}
|
|
onChange={(e) => setStoreForm({ ...storeForm, email: e.target.value })}
|
|
className="pl-10 pr-4 py-2.5 w-full border border-slate-200 rounded-xl bg-slate-50/40 hover:bg-slate-100/60 focus:bg-white outline-none focus:ring-4 focus:ring-purple-100 focus:border-purple-600 transition-all font-semibold text-xs text-slate-800 shadow-sm"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] font-bold text-slate-500 uppercase tracking-widest block mb-1">Outlet Contact Number</label>
|
|
<div className="relative rounded-xl">
|
|
<div className="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none text-slate-400">
|
|
<Phone size={14} />
|
|
</div>
|
|
<input
|
|
type="text"
|
|
maxLength={10}
|
|
placeholder="e.g. 9876543222"
|
|
value={storeForm.contactno}
|
|
onChange={(e) => setStoreForm({ ...storeForm, contactno: e.target.value.replace(/\D/g, '') })}
|
|
className="pl-10 pr-4 py-2.5 w-full border border-slate-200 rounded-xl bg-slate-50/40 hover:bg-slate-100/60 focus:bg-white outline-none focus:ring-4 focus:ring-purple-100 focus:border-purple-600 transition-all font-semibold text-xs text-slate-800 shadow-sm"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* SECTION 2: Location & Address */}
|
|
<div className="bg-white border border-slate-200/60 p-5 rounded-2xl space-y-4 shadow-sm">
|
|
<h4 className="text-xs font-black text-purple-700 uppercase tracking-widest flex items-center gap-1.5 pb-2 border-b border-slate-100/60">
|
|
<MapPin size={13} /> Location & Address Details
|
|
</h4>
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] font-bold text-slate-500 uppercase tracking-widest block mb-1">Store Branch Address</label>
|
|
<div className="relative rounded-xl">
|
|
<div className="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none text-slate-400">
|
|
<MapPin size={14} />
|
|
</div>
|
|
<input
|
|
type="text"
|
|
placeholder="e.g. 240, DB Road"
|
|
value={storeForm.address}
|
|
onChange={(e) => setStoreForm({ ...storeForm, address: e.target.value })}
|
|
className="pl-10 pr-4 py-2.5 w-full border border-slate-200 rounded-xl bg-slate-50/40 hover:bg-slate-100/60 focus:bg-white outline-none focus:ring-4 focus:ring-purple-100 focus:border-purple-600 transition-all font-semibold text-xs text-slate-800 shadow-sm"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4">
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] font-bold text-slate-500 uppercase tracking-widest block mb-1">Suburb</label>
|
|
<input
|
|
type="text"
|
|
placeholder="e.g. RS Puram"
|
|
value={storeForm.suburb}
|
|
onChange={(e) => setStoreForm({ ...storeForm, suburb: e.target.value })}
|
|
className="px-4 py-2.5 w-full border border-slate-200 rounded-xl bg-slate-50/40 hover:bg-slate-100/60 focus:bg-white outline-none focus:ring-4 focus:ring-purple-100 focus:border-purple-600 transition-all font-semibold text-xs text-slate-800 shadow-sm"
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] font-bold text-slate-500 uppercase tracking-widest block mb-1">City</label>
|
|
<input
|
|
type="text"
|
|
value={storeForm.city}
|
|
onChange={(e) => setStoreForm({ ...storeForm, city: e.target.value })}
|
|
className="px-4 py-2.5 w-full border border-slate-200 rounded-xl bg-slate-50/40 hover:bg-slate-100/60 focus:bg-white outline-none focus:ring-4 focus:ring-purple-100 focus:border-purple-600 transition-all font-semibold text-xs text-slate-800 shadow-sm"
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] font-bold text-slate-500 uppercase tracking-widest block mb-1">State</label>
|
|
<input
|
|
type="text"
|
|
value={storeForm.state}
|
|
onChange={(e) => setStoreForm({ ...storeForm, state: e.target.value })}
|
|
className="px-4 py-2.5 w-full border border-slate-200 rounded-xl bg-slate-50/40 hover:bg-slate-100/60 focus:bg-white outline-none focus:ring-4 focus:ring-purple-100 focus:border-purple-600 transition-all font-semibold text-xs text-slate-800 shadow-sm"
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] font-bold text-slate-500 uppercase tracking-widest block mb-1">Postcode</label>
|
|
<input
|
|
type="text"
|
|
placeholder="e.g. 641002"
|
|
value={storeForm.postcode}
|
|
onChange={(e) => setStoreForm({ ...storeForm, postcode: e.target.value })}
|
|
className="px-4 py-2.5 w-full border border-slate-200 rounded-xl bg-slate-50/40 hover:bg-slate-100/60 focus:bg-white outline-none focus:ring-4 focus:ring-purple-100 focus:border-purple-600 transition-all font-semibold text-xs text-slate-800 shadow-sm"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* SECTION 3: Operations & Logistics */}
|
|
<div className="bg-white border border-slate-200/60 p-5 rounded-2xl space-y-4 shadow-sm">
|
|
<h4 className="text-xs font-black text-purple-700 uppercase tracking-widest flex items-center gap-1.5 pb-2 border-b border-slate-100/60">
|
|
<Navigation size={13} /> Logistics & Operational Hours
|
|
</h4>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4">
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] font-bold text-slate-500 uppercase tracking-widest block mb-1 flex items-center gap-1"><Clock size={12} className="text-slate-400" /> Open Time</label>
|
|
<div className="relative rounded-xl">
|
|
<div className="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none text-slate-400">
|
|
<Clock size={14} />
|
|
</div>
|
|
<input
|
|
type="text"
|
|
placeholder="06:00:00"
|
|
value={storeForm.opentime}
|
|
onChange={(e) => setStoreForm({ ...storeForm, opentime: e.target.value })}
|
|
className="pl-10 pr-4 py-2.5 w-full border border-slate-200 rounded-xl bg-slate-50/40 focus:bg-white outline-none focus:ring-4 focus:ring-purple-100 focus:border-purple-600 transition-all font-semibold text-xs text-slate-800 shadow-sm"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] font-bold text-slate-500 uppercase tracking-widest block mb-1 flex items-center gap-1"><Clock size={12} className="text-slate-400" /> Close Time</label>
|
|
<div className="relative rounded-xl">
|
|
<div className="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none text-slate-400">
|
|
<Clock size={14} />
|
|
</div>
|
|
<input
|
|
type="text"
|
|
placeholder="22:00:00"
|
|
value={storeForm.closetime}
|
|
onChange={(e) => setStoreForm({ ...storeForm, closetime: e.target.value })}
|
|
className="pl-10 pr-4 py-2.5 w-full border border-slate-200 rounded-xl bg-slate-50/40 focus:bg-white outline-none focus:ring-4 focus:ring-purple-100 focus:border-purple-600 transition-all font-semibold text-xs text-slate-800 shadow-sm"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] font-bold text-slate-500 uppercase tracking-widest block mb-1 flex items-center gap-1"><Navigation size={12} className="text-slate-400" /> Delivery Radius (m)</label>
|
|
<div className="relative rounded-xl">
|
|
<div className="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none text-slate-400">
|
|
<Navigation size={14} />
|
|
</div>
|
|
<input
|
|
type="number"
|
|
value={storeForm.deliveryradius}
|
|
onChange={(e) => setStoreForm({ ...storeForm, deliveryradius: Number(e.target.value) })}
|
|
className="pl-10 pr-4 py-2.5 w-full border border-slate-200 rounded-xl bg-slate-50/40 focus:bg-white outline-none focus:ring-4 focus:ring-purple-100 focus:border-purple-600 transition-all font-semibold text-xs text-slate-800 shadow-sm"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] font-bold text-slate-500 uppercase tracking-widest block mb-1 flex items-center gap-1"><Clock size={12} className="text-slate-400" /> Est. Speed (mins)</label>
|
|
<div className="relative rounded-xl">
|
|
<div className="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none text-slate-400">
|
|
<Clock size={14} />
|
|
</div>
|
|
<input
|
|
type="number"
|
|
value={storeForm.deliverymins}
|
|
onChange={(e) => setStoreForm({ ...storeForm, deliverymins: Number(e.target.value) })}
|
|
className="pl-10 pr-4 py-2.5 w-full border border-slate-200 rounded-xl bg-slate-50/40 focus:bg-white outline-none focus:ring-4 focus:ring-purple-100 focus:border-purple-600 transition-all font-semibold text-xs text-slate-800 shadow-sm"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* FORM ACTIONS */}
|
|
<div className="flex justify-end gap-3 pt-2">
|
|
{onBack && (
|
|
<button
|
|
type="button"
|
|
onClick={onBack}
|
|
className="px-5 py-3 border border-slate-200 hover:bg-slate-50 hover:border-slate-300 bg-white rounded-xl text-xs font-bold uppercase tracking-wider cursor-pointer text-slate-500 hover:text-slate-700 transition-all flex items-center gap-1.5 shadow-sm active:scale-95"
|
|
>
|
|
<ArrowLeft size={14} />
|
|
Back
|
|
</button>
|
|
)}
|
|
<button
|
|
type="submit"
|
|
disabled={createLocationMut.isPending}
|
|
className="bg-gradient-to-r from-[#662582] to-indigo-650 hover:from-purple-755 hover:to-indigo-700 text-white px-6 py-3 rounded-xl text-xs font-bold uppercase tracking-wider flex items-center gap-2 cursor-pointer shadow-md active:scale-95 transition-all border-none"
|
|
>
|
|
{createLocationMut.isPending ? (
|
|
<>
|
|
<Loader2 size={14} className="animate-spin" />
|
|
Commissioning Hub...
|
|
</>
|
|
) : (
|
|
<>
|
|
<Store size={14} />
|
|
Commission Branch
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* TAB 3: Rider Onboarding */}
|
|
{activeTab === 'rider' && (
|
|
<div className="bg-white border border-slate-200/60 p-6 rounded-2xl shadow-sm space-y-6">
|
|
<div>
|
|
<h3 className="text-lg font-bold text-slate-900 flex items-center gap-2">
|
|
<Bike className="text-purple-600" />
|
|
Onboard Delivery Rider
|
|
</h3>
|
|
<p className="text-slate-500 text-xs mt-1">
|
|
Onboarding riders is a multi-table database transaction. We register the user profile in <code>app_users</code>, and dynamically generate SQL configuration queries to inject vehicle, shift, and availability settings.
|
|
</p>
|
|
</div>
|
|
|
|
{riderSuccess ? (
|
|
<div className="space-y-5 animate-in slide-in-from-bottom duration-300">
|
|
<div className="bg-emerald-50 border border-emerald-100 rounded-2xl p-5 flex items-center gap-4">
|
|
<div className="w-10 h-10 rounded-full bg-emerald-500 text-white flex items-center justify-center text-lg font-bold shrink-0">✓</div>
|
|
<div>
|
|
<h4 className="font-bold text-emerald-950 text-sm">Base Account Registered successfully!</h4>
|
|
<p className="text-xs text-emerald-700 mt-0.5">
|
|
Rider user created in <strong>app_users</strong> with ID: <strong>#{riderSuccess.userid}</strong>.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* SQL generated panel */}
|
|
<div className="space-y-2">
|
|
<div className="flex justify-between items-center select-none">
|
|
<span className="text-[10px] font-bold text-slate-500 uppercase tracking-widest flex items-center gap-1.5">
|
|
<FileText size={12} className="text-slate-400" /> Administrative SQL Synchronization script
|
|
</span>
|
|
<button
|
|
type="button"
|
|
onClick={handleCopySql}
|
|
className="flex items-center gap-1 text-[11px] font-bold text-purple-650 bg-purple-50 hover:bg-purple-100 border border-purple-200 px-3 py-1.5 rounded-lg cursor-pointer transition-all active:scale-95"
|
|
>
|
|
{copiedSql ? (
|
|
<>
|
|
<Check size={12} className="text-emerald-650" />
|
|
Copied!
|
|
</>
|
|
) : (
|
|
<>
|
|
<Copy size={12} />
|
|
Copy Script
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
|
|
<pre className="bg-slate-900 text-slate-100 rounded-xl p-4 text-xs font-mono overflow-x-auto shadow-inner leading-relaxed border border-slate-950 max-h-[16rem]">
|
|
{riderSuccess.sql}
|
|
</pre>
|
|
|
|
<div className="bg-amber-50 border border-amber-100 rounded-xl p-4 flex gap-3 text-xs leading-relaxed text-amber-800">
|
|
<ShieldAlert size={16} className="text-amber-500 shrink-0 mt-0.5" />
|
|
<div>
|
|
<span className="font-bold block mb-0.5">Immediate Action Required:</span>
|
|
Please share this SQL script with the Database Administrator (DBA) or run it against the Postgres instance to complete vehicle assignments, shifts, and active pool configuration.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-end pt-2">
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setRiderSuccess(null);
|
|
setRiderForm({
|
|
firstname: '',
|
|
lastname: '',
|
|
email: '',
|
|
contactno: '',
|
|
password: 'Rider@123',
|
|
locationid: 0,
|
|
shiftid: 0,
|
|
vehiclename: '',
|
|
vehiclemodel: '',
|
|
licensenumber: '',
|
|
});
|
|
}}
|
|
className="bg-purple-650 hover:bg-purple-755 text-white font-bold text-xs uppercase tracking-wider px-5 py-2.5 rounded-lg border-none cursor-pointer"
|
|
>
|
|
Onboard Another Rider
|
|
</button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<form onSubmit={handleRiderSubmit} className="space-y-4">
|
|
{/* Account personal details */}
|
|
<div className="bg-slate-50/50 p-4 rounded-xl border border-slate-150 space-y-4">
|
|
<h4 className="font-bold text-xs uppercase tracking-wider text-slate-600">1. Base Profile Details</h4>
|
|
<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">First Name (*)</label>
|
|
<input
|
|
type="text"
|
|
placeholder="e.g. Karthik"
|
|
value={riderForm.firstname}
|
|
onChange={(e) => setRiderForm({ ...riderForm, firstname: e.target.value })}
|
|
className="w-full border border-slate-250 rounded-xl p-3 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">Last Name</label>
|
|
<input
|
|
type="text"
|
|
placeholder="e.g. Sundar"
|
|
value={riderForm.lastname}
|
|
onChange={(e) => setRiderForm({ ...riderForm, lastname: e.target.value })}
|
|
className="w-full border border-slate-250 rounded-xl p-3 bg-white outline-none focus:border-purple-500 transition-all font-semibold text-xs text-slate-800"
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] font-bold text-slate-450 uppercase tracking-widest flex items-center gap-1"><Mail size={11} /> Email (*)</label>
|
|
<input
|
|
type="email"
|
|
placeholder="e.g. karthik@rider.com"
|
|
value={riderForm.email}
|
|
onChange={(e) => setRiderForm({ ...riderForm, email: e.target.value })}
|
|
className="w-full border border-slate-250 rounded-xl p-3 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 flex items-center gap-1"><Phone size={11} /> Contact No (*)</label>
|
|
<input
|
|
type="text"
|
|
maxLength={10}
|
|
placeholder="e.g. 9988776655"
|
|
value={riderForm.contactno}
|
|
onChange={(e) => setRiderForm({ ...riderForm, contactno: e.target.value.replace(/\D/g, '') })}
|
|
className="w-full border border-slate-250 rounded-xl p-3 bg-white outline-none focus:border-purple-500 transition-all font-semibold text-xs text-slate-800"
|
|
required
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Vehicle, Shift & Hub configurations */}
|
|
<div className="bg-slate-50/50 p-4 rounded-xl border border-slate-150 space-y-4">
|
|
<h4 className="font-bold text-xs uppercase tracking-wider text-slate-600">2. Fleet & Shift configuration</h4>
|
|
<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">Assign Store Hub (*)</label>
|
|
<select
|
|
value={riderForm.locationid}
|
|
onChange={(e) => setRiderForm({ ...riderForm, locationid: Number(e.target.value) })}
|
|
className="w-full border border-slate-250 rounded-xl p-3 bg-white outline-none focus:border-purple-500 transition-all font-semibold text-xs text-slate-800 cursor-pointer"
|
|
required
|
|
>
|
|
<option value={0}>Select Hub location...</option>
|
|
{locations.map((loc) => (
|
|
<option key={loc.locationid} value={Number(loc.locationid)}>{fstr(loc.locationname)}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] font-bold text-slate-450 uppercase tracking-widest">Assigned Shift</label>
|
|
<select
|
|
value={riderForm.shiftid}
|
|
onChange={(e) => setRiderForm({ ...riderForm, shiftid: Number(e.target.value) })}
|
|
className="w-full border border-slate-250 rounded-xl p-3 bg-white outline-none focus:border-purple-500 transition-all font-semibold text-xs text-slate-800 cursor-pointer"
|
|
>
|
|
<option value={0}>Select Shift timings...</option>
|
|
{shiftsQ.isLoading ? (
|
|
<option>Loading shifts...</option>
|
|
) : (shiftsQ.data || []).map((sh: any) => (
|
|
<option key={sh.shiftid} value={fnum(sh.shiftid)}>{fstr(sh.shiftname)}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] font-bold text-slate-450 uppercase tracking-widest">Vehicle Name</label>
|
|
<input
|
|
type="text"
|
|
placeholder="e.g. Coimbatore Hub Bike 4"
|
|
value={riderForm.vehiclename}
|
|
onChange={(e) => setRiderForm({ ...riderForm, vehiclename: e.target.value })}
|
|
className="w-full border border-slate-250 rounded-xl p-3 bg-white outline-none focus:border-purple-500 transition-all font-semibold text-xs text-slate-800"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] font-bold text-slate-450 uppercase tracking-widest">Vehicle Model</label>
|
|
<input
|
|
type="text"
|
|
placeholder="e.g. TVS iQube Electric"
|
|
value={riderForm.vehiclemodel}
|
|
onChange={(e) => setRiderForm({ ...riderForm, vehiclemodel: e.target.value })}
|
|
className="w-full border border-slate-250 rounded-xl p-3 bg-white outline-none focus:border-purple-500 transition-all font-semibold text-xs text-slate-800"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-1 md:col-span-2">
|
|
<label className="text-[10px] font-bold text-slate-450 uppercase tracking-widest">License Number</label>
|
|
<input
|
|
type="text"
|
|
placeholder="e.g. TN-37-BY-1234"
|
|
value={riderForm.licensenumber}
|
|
onChange={(e) => setRiderForm({ ...riderForm, licensenumber: e.target.value })}
|
|
className="w-full border border-slate-250 rounded-xl p-3 bg-white outline-none focus:border-purple-500 transition-all font-semibold text-xs text-slate-800"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-end pt-4">
|
|
<button
|
|
type="submit"
|
|
disabled={createUserMut.isPending}
|
|
className="bg-purple-650 hover:bg-purple-755 text-white px-6 py-3 rounded-xl text-xs font-bold uppercase tracking-wider flex items-center gap-2 cursor-pointer shadow-sm active:scale-95 transition-all border-none"
|
|
>
|
|
{createUserMut.isPending ? (
|
|
<>
|
|
<Loader2 size={14} className="animate-spin" />
|
|
Registering User Profile...
|
|
</>
|
|
) : (
|
|
<>
|
|
<Check size={14} />
|
|
Onboard Rider Account
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-lg animate-in fade-in duration-300 font-sans text-slate-700">
|
|
|
|
{/* Tabs Layout */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-4 gap-gutter items-start">
|
|
{/* Navigation Tabs */}
|
|
<div className="lg:col-span-1 space-y-md bg-slate-50/50 border border-slate-200/60 p-4 rounded-2xl shadow-sm">
|
|
<nav className="flex lg:flex-col gap-1.5 overflow-x-auto select-none">
|
|
<button
|
|
onClick={() => { setActiveTab('tenant'); setTenantSuccess(null); }}
|
|
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 w-full ${
|
|
activeTab === 'tenant'
|
|
? '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'
|
|
}`}
|
|
>
|
|
<Building2 size={16} className={activeTab === 'tenant' ? 'text-[#662582]' : 'text-slate-450'} />
|
|
<span>Tenant Onboarding</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => { setActiveTab('store'); setStoreSuccess(null); }}
|
|
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 w-full ${
|
|
activeTab === 'store'
|
|
? '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'
|
|
}`}
|
|
>
|
|
<Store size={16} className={activeTab === 'store' ? 'text-[#662582]' : 'text-slate-450'} />
|
|
<span>Store Branch Onboarding</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => { setActiveTab('rider'); setRiderSuccess(null); }}
|
|
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 w-full ${
|
|
activeTab === 'rider'
|
|
? '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'
|
|
}`}
|
|
>
|
|
<Bike size={16} className={activeTab === 'rider' ? 'text-[#662582]' : 'text-slate-450'} />
|
|
<span>Rider Onboarding</span>
|
|
</button>
|
|
</nav>
|
|
</div>
|
|
|
|
{/* Form Panel */}
|
|
<div className="lg:col-span-3 space-y-gutter">
|
|
{/* TAB 1: Tenant Onboarding */}
|
|
{activeTab === 'tenant' && (
|
|
<div className="bg-white border border-slate-200/60 p-6 rounded-2xl shadow-sm space-y-6">
|
|
<div>
|
|
<h3 className="text-lg font-bold text-slate-900">Provision New Merchant Tenant</h3>
|
|
<p className="text-slate-500 text-xs mt-1">
|
|
Onboard a new merchant group. This registers their enterprise, defaults the order serialization, and spawns the primary Administrator account.
|
|
</p>
|
|
</div>
|
|
|
|
{tenantSuccess ? (
|
|
<div className="bg-emerald-50 border border-emerald-100 rounded-2xl p-6 text-center space-y-4">
|
|
<div className="w-12 h-12 rounded-full bg-emerald-550 text-white flex items-center justify-center mx-auto text-xl font-bold">✓</div>
|
|
<h4 className="font-bold text-emerald-900 text-base">Onboarding Complete!</h4>
|
|
<p className="text-xs text-emerald-700 max-w-md mx-auto">
|
|
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(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
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<form onSubmit={handleTenantSubmit} className="space-y-4">
|
|
<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">Merchant Name (*)</label>
|
|
<input
|
|
type="text"
|
|
placeholder="e.g. Kaveri Groceries"
|
|
value={tenantForm.tenantname}
|
|
onChange={(e) => setTenantForm({ ...tenantForm, tenantname: 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">Company Registered Name (*)</label>
|
|
<input
|
|
type="text"
|
|
placeholder="e.g. Kaveri Retail Pvt. Ltd."
|
|
value={tenantForm.companyname}
|
|
onChange={(e) => setTenantForm({ ...tenantForm, companyname: 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">Primary Phone Number (*)</label>
|
|
<input
|
|
type="text"
|
|
maxLength={10}
|
|
placeholder="e.g. 9876543210"
|
|
value={tenantForm.primarycontact}
|
|
onChange={(e) => setTenantForm({ ...tenantForm, primarycontact: e.target.value.replace(/\D/g, '') })}
|
|
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">Primary Admin Email (*)</label>
|
|
<input
|
|
type="email"
|
|
placeholder="e.g. admin@kaveri.com"
|
|
value={tenantForm.primaryemail}
|
|
onChange={(e) => setTenantForm({ ...tenantForm, primaryemail: 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>
|
|
|
|
<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
|
|
type="text"
|
|
placeholder="e.g. 12, Avinashi Road"
|
|
value={tenantForm.address}
|
|
onChange={(e) => setTenantForm({ ...tenantForm, address: 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"
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] font-bold text-slate-450 uppercase tracking-widest">Suburb</label>
|
|
<input
|
|
type="text"
|
|
placeholder="e.g. Peelamedu"
|
|
value={tenantForm.suburb}
|
|
onChange={(e) => setTenantForm({ ...tenantForm, suburb: 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"
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] font-bold text-slate-450 uppercase tracking-widest">City</label>
|
|
<input
|
|
type="text"
|
|
value={tenantForm.city}
|
|
onChange={(e) => setTenantForm({ ...tenantForm, city: 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"
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] font-bold text-slate-450 uppercase tracking-widest">State</label>
|
|
<input
|
|
type="text"
|
|
value={tenantForm.state}
|
|
onChange={(e) => setTenantForm({ ...tenantForm, state: 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"
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] font-bold text-slate-450 uppercase tracking-widest">Postcode</label>
|
|
<input
|
|
type="text"
|
|
placeholder="e.g. 641004"
|
|
value={tenantForm.postcode}
|
|
onChange={(e) => setTenantForm({ ...tenantForm, postcode: 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"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-end pt-4">
|
|
<button
|
|
type="submit"
|
|
disabled={createTenantMut.isPending}
|
|
className="bg-purple-650 hover:bg-purple-755 text-white px-6 py-3 rounded-xl text-xs font-bold uppercase tracking-wider flex items-center gap-2 cursor-pointer shadow-sm active:scale-95 transition-all border-none"
|
|
>
|
|
{createTenantMut.isPending ? (
|
|
<>
|
|
<Loader2 size={14} className="animate-spin" />
|
|
Provisioning Tenant...
|
|
</>
|
|
) : (
|
|
<>
|
|
<Building2 size={14} />
|
|
Provision Tenant
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* TAB 2: Store Onboarding */}
|
|
{activeTab === 'store' && (
|
|
<div className="bg-white border border-slate-200/60 p-6 rounded-2xl shadow-sm space-y-6">
|
|
<div>
|
|
<h3 className="text-lg font-bold text-slate-900">Add Store Outlet Location</h3>
|
|
<p className="text-slate-500 text-xs mt-1">
|
|
Commission a new store branch/hub under a tenant. This sets up store parameters, delivery thresholds, and spawns an placeholder branch manager account.
|
|
</p>
|
|
</div>
|
|
|
|
{storeSuccess ? (
|
|
<div className="bg-emerald-50 border border-emerald-100 rounded-2xl p-6 text-center space-y-4">
|
|
<div className="w-12 h-12 rounded-full bg-emerald-550 text-white flex items-center justify-center mx-auto text-xl font-bold">✓</div>
|
|
<h4 className="font-bold text-emerald-900 text-base">Store Branch Active!</h4>
|
|
<p className="text-xs text-emerald-700 max-w-md mx-auto">
|
|
Store <strong>{storeForm.locationname}</strong> has been initialized. A default placeholder manager user has been spawned in inactive mode (requires password setup).
|
|
</p>
|
|
<div className="flex justify-center gap-3">
|
|
{onBack && (
|
|
<button
|
|
type="button"
|
|
onClick={onBack}
|
|
className="bg-white border border-slate-200 hover:bg-slate-50 text-slate-600 font-bold text-xs uppercase tracking-wider px-5 py-2.5 rounded-lg cursor-pointer"
|
|
>
|
|
Back to Outlets
|
|
</button>
|
|
)}
|
|
<button
|
|
onClick={() => { setStoreSuccess(null); setStoreForm({ tenantid: FIESTA_TENANT_ID, locationname: '', address: '', suburb: '', city: 'Coimbatore', state: 'Tamil Nadu', postcode: '', contactno: '', email: '', opentime: '06:00:00', closetime: '22:00:00', deliverymins: 45, deliveryradius: 5000 }); }}
|
|
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"
|
|
>
|
|
Add Another Branch
|
|
</button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<form onSubmit={handleStoreSubmit} className="space-y-4">
|
|
<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">Select Owner Tenant (*)</label>
|
|
<select
|
|
value={storeForm.tenantid}
|
|
onChange={(e) => setStoreForm({ ...storeForm, tenantid: 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"
|
|
>
|
|
{tenantsQ.isLoading ? (
|
|
<option>Loading tenants...</option>
|
|
) : (tenantsQ.data || []).map((t) => (
|
|
<option key={t.tenantid} value={Number(t.tenantid)}>{fstr(t.tenantname)} (#{fstr(t.tenantid)})</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] font-bold text-slate-450 uppercase tracking-widest">Store Outlet Name (*)</label>
|
|
<input
|
|
type="text"
|
|
placeholder="e.g. RS Puram Super Hub"
|
|
value={storeForm.locationname}
|
|
onChange={(e) => setStoreForm({ ...storeForm, 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">Outlet Email Address</label>
|
|
<input
|
|
type="email"
|
|
placeholder="e.g. rspuram@kaveri.com"
|
|
value={storeForm.email}
|
|
onChange={(e) => setStoreForm({ ...storeForm, email: 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"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] font-bold text-slate-450 uppercase tracking-widest">Outlet Contact Number</label>
|
|
<input
|
|
type="text"
|
|
maxLength={10}
|
|
placeholder="e.g. 9876543222"
|
|
value={storeForm.contactno}
|
|
onChange={(e) => setStoreForm({ ...storeForm, contactno: e.target.value.replace(/\D/g, '') })}
|
|
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"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] font-bold text-slate-450 uppercase tracking-widest">Store Branch Address</label>
|
|
<input
|
|
type="text"
|
|
placeholder="e.g. 240, DB Road"
|
|
value={storeForm.address}
|
|
onChange={(e) => setStoreForm({ ...storeForm, address: 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"
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] font-bold text-slate-450 uppercase tracking-widest">Suburb</label>
|
|
<input
|
|
type="text"
|
|
placeholder="e.g. RS Puram"
|
|
value={storeForm.suburb}
|
|
onChange={(e) => setStoreForm({ ...storeForm, suburb: 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"
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] font-bold text-slate-450 uppercase tracking-widest">City</label>
|
|
<input
|
|
type="text"
|
|
value={storeForm.city}
|
|
onChange={(e) => setStoreForm({ ...storeForm, city: 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"
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] font-bold text-slate-450 uppercase tracking-widest">State</label>
|
|
<input
|
|
type="text"
|
|
value={storeForm.state}
|
|
onChange={(e) => setStoreForm({ ...storeForm, state: 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"
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] font-bold text-slate-450 uppercase tracking-widest">Postcode</label>
|
|
<input
|
|
type="text"
|
|
placeholder="e.g. 641002"
|
|
value={storeForm.postcode}
|
|
onChange={(e) => setStoreForm({ ...storeForm, postcode: 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"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 pt-2 border-t border-slate-100">
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] font-bold text-slate-450 uppercase tracking-widest flex items-center gap-1"><Clock size={12} /> Open Time</label>
|
|
<input
|
|
type="text"
|
|
placeholder="06:00:00"
|
|
value={storeForm.opentime}
|
|
onChange={(e) => setStoreForm({ ...storeForm, opentime: e.target.value })}
|
|
className="w-full border border-slate-250 rounded-xl p-3 bg-slate-50/40 focus:bg-white outline-none focus:border-purple-500 transition-all font-semibold text-xs text-slate-800"
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] font-bold text-slate-450 uppercase tracking-widest flex items-center gap-1"><Clock size={12} /> Close Time</label>
|
|
<input
|
|
type="text"
|
|
placeholder="22:00:00"
|
|
value={storeForm.closetime}
|
|
onChange={(e) => setStoreForm({ ...storeForm, closetime: e.target.value })}
|
|
className="w-full border border-slate-250 rounded-xl p-3 bg-slate-50/40 focus:bg-white outline-none focus:border-purple-500 transition-all font-semibold text-xs text-slate-800"
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] font-bold text-slate-450 uppercase tracking-widest flex items-center gap-1"><Navigation size={12} /> Delivery Radius (m)</label>
|
|
<input
|
|
type="number"
|
|
value={storeForm.deliveryradius}
|
|
onChange={(e) => setStoreForm({ ...storeForm, deliveryradius: Number(e.target.value) })}
|
|
className="w-full border border-slate-250 rounded-xl p-3 bg-slate-50/40 focus:bg-white outline-none focus:border-purple-500 transition-all font-semibold text-xs text-slate-800"
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] font-bold text-slate-450 uppercase tracking-widest flex items-center gap-1"><Clock size={12} /> Est. Speed (mins)</label>
|
|
<input
|
|
type="number"
|
|
value={storeForm.deliverymins}
|
|
onChange={(e) => setStoreForm({ ...storeForm, deliverymins: Number(e.target.value) })}
|
|
className="w-full border border-slate-250 rounded-xl p-3 bg-slate-50/40 focus:bg-white outline-none focus:border-purple-500 transition-all font-semibold text-xs text-slate-800"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-end gap-3 pt-4">
|
|
{onBack && (
|
|
<button
|
|
type="button"
|
|
onClick={onBack}
|
|
className="px-5 py-3 border border-slate-200 hover:bg-slate-55 bg-white rounded-xl text-xs font-bold uppercase tracking-wider cursor-pointer text-slate-500 hover:text-slate-700 transition-all border-none"
|
|
>
|
|
Back
|
|
</button>
|
|
)}
|
|
<button
|
|
type="submit"
|
|
disabled={createLocationMut.isPending}
|
|
className="bg-purple-650 hover:bg-purple-755 text-white px-6 py-3 rounded-xl text-xs font-bold uppercase tracking-wider flex items-center gap-2 cursor-pointer shadow-sm active:scale-95 transition-all border-none"
|
|
>
|
|
{createLocationMut.isPending ? (
|
|
<>
|
|
<Loader2 size={14} className="animate-spin" />
|
|
Commissioning Hub...
|
|
</>
|
|
) : (
|
|
<>
|
|
<Store size={14} />
|
|
Commission Branch
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* TAB 3: Rider Onboarding */}
|
|
{activeTab === 'rider' && (
|
|
<div className="bg-white border border-slate-200/60 p-6 rounded-2xl shadow-sm space-y-6">
|
|
<div>
|
|
<h3 className="text-lg font-bold text-slate-900 flex items-center gap-2">
|
|
<Bike className="text-purple-600" />
|
|
Onboard Delivery Rider
|
|
</h3>
|
|
<p className="text-slate-500 text-xs mt-1">
|
|
Onboarding riders is a multi-table database transaction. We register the user profile in <code>app_users</code>, and dynamically generate SQL configuration queries to inject vehicle, shift, and availability settings.
|
|
</p>
|
|
</div>
|
|
|
|
{riderSuccess ? (
|
|
<div className="space-y-5 animate-in slide-in-from-bottom duration-300">
|
|
<div className="bg-emerald-50 border border-emerald-100 rounded-2xl p-5 flex items-center gap-4">
|
|
<div className="w-10 h-10 rounded-full bg-emerald-500 text-white flex items-center justify-center text-lg font-bold shrink-0">✓</div>
|
|
<div>
|
|
<h4 className="font-bold text-emerald-950 text-sm">Base Account Registered successfully!</h4>
|
|
<p className="text-xs text-emerald-700 mt-0.5">
|
|
Rider user created in <strong>app_users</strong> with ID: <strong>#{riderSuccess.userid}</strong>.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* SQL generated panel */}
|
|
<div className="space-y-2">
|
|
<div className="flex justify-between items-center select-none">
|
|
<span className="text-[10px] font-bold text-slate-500 uppercase tracking-widest flex items-center gap-1.5">
|
|
<FileText size={12} className="text-slate-400" /> Administrative SQL Synchronization script
|
|
</span>
|
|
<button
|
|
onClick={handleCopySql}
|
|
className="flex items-center gap-1 text-[11px] font-bold text-purple-650 bg-purple-50 hover:bg-purple-100 border border-purple-200 px-3 py-1.5 rounded-lg cursor-pointer transition-all active:scale-95"
|
|
>
|
|
{copiedSql ? (
|
|
<>
|
|
<Check size={12} className="text-emerald-650" />
|
|
Copied!
|
|
</>
|
|
) : (
|
|
<>
|
|
<Copy size={12} />
|
|
Copy Script
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
|
|
<pre className="bg-slate-900 text-slate-100 rounded-xl p-4 text-xs font-mono overflow-x-auto shadow-inner leading-relaxed border border-slate-950 max-h-[16rem]">
|
|
{riderSuccess.sql}
|
|
</pre>
|
|
|
|
<div className="bg-amber-50 border border-amber-100 rounded-xl p-4 flex gap-3 text-xs leading-relaxed text-amber-800">
|
|
<ShieldAlert size={16} className="text-amber-500 shrink-0 mt-0.5" />
|
|
<div>
|
|
<span className="font-bold block mb-0.5">Immediate Action Required:</span>
|
|
Please share this SQL script with the Database Administrator (DBA) or run it against the Postgres instance to complete vehicle assignments, shifts, and active pool configuration.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-end pt-2">
|
|
<button
|
|
onClick={() => {
|
|
setRiderSuccess(null);
|
|
setRiderForm({
|
|
firstname: '',
|
|
lastname: '',
|
|
email: '',
|
|
contactno: '',
|
|
password: 'Rider@123',
|
|
locationid: 0,
|
|
shiftid: 0,
|
|
vehiclename: '',
|
|
vehiclemodel: '',
|
|
licensenumber: '',
|
|
});
|
|
}}
|
|
className="bg-purple-650 hover:bg-purple-755 text-white font-bold text-xs uppercase tracking-wider px-5 py-2.5 rounded-lg border-none cursor-pointer"
|
|
>
|
|
Onboard Another Rider
|
|
</button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<form onSubmit={handleRiderSubmit} className="space-y-4">
|
|
{/* Account personal details */}
|
|
<div className="bg-slate-50/50 p-4 rounded-xl border border-slate-150 space-y-4">
|
|
<h4 className="font-bold text-xs uppercase tracking-wider text-slate-600">1. Base Profile Details</h4>
|
|
<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">First Name (*)</label>
|
|
<input
|
|
type="text"
|
|
placeholder="e.g. Karthik"
|
|
value={riderForm.firstname}
|
|
onChange={(e) => setRiderForm({ ...riderForm, firstname: e.target.value })}
|
|
className="w-full border border-slate-250 rounded-xl p-3 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">Last Name</label>
|
|
<input
|
|
type="text"
|
|
placeholder="e.g. Sundar"
|
|
value={riderForm.lastname}
|
|
onChange={(e) => setRiderForm({ ...riderForm, lastname: e.target.value })}
|
|
className="w-full border border-slate-250 rounded-xl p-3 bg-white outline-none focus:border-purple-500 transition-all font-semibold text-xs text-slate-800"
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] font-bold text-slate-450 uppercase tracking-widest flex items-center gap-1"><Mail size={11} /> Email (*)</label>
|
|
<input
|
|
type="email"
|
|
placeholder="e.g. karthik@rider.com"
|
|
value={riderForm.email}
|
|
onChange={(e) => setRiderForm({ ...riderForm, email: e.target.value })}
|
|
className="w-full border border-slate-250 rounded-xl p-3 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 flex items-center gap-1"><Phone size={11} /> Contact No (*)</label>
|
|
<input
|
|
type="text"
|
|
maxLength={10}
|
|
placeholder="e.g. 9988776655"
|
|
value={riderForm.contactno}
|
|
onChange={(e) => setRiderForm({ ...riderForm, contactno: e.target.value.replace(/\D/g, '') })}
|
|
className="w-full border border-slate-250 rounded-xl p-3 bg-white outline-none focus:border-purple-500 transition-all font-semibold text-xs text-slate-800"
|
|
required
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Vehicle, Shift & Hub configurations */}
|
|
<div className="bg-slate-50/50 p-4 rounded-xl border border-slate-150 space-y-4">
|
|
<h4 className="font-bold text-xs uppercase tracking-wider text-slate-600">2. Fleet & Shift configuration</h4>
|
|
<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">Assign Store Hub (*)</label>
|
|
<select
|
|
value={riderForm.locationid}
|
|
onChange={(e) => setRiderForm({ ...riderForm, locationid: Number(e.target.value) })}
|
|
className="w-full border border-slate-250 rounded-xl p-3 bg-white outline-none focus:border-purple-500 transition-all font-semibold text-xs text-slate-800 cursor-pointer"
|
|
required
|
|
>
|
|
<option value={0}>Select Hub location...</option>
|
|
{locations.map((loc) => (
|
|
<option key={loc.locationid} value={Number(loc.locationid)}>{fstr(loc.locationname)}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] font-bold text-slate-450 uppercase tracking-widest">Assigned Shift</label>
|
|
<select
|
|
value={riderForm.shiftid}
|
|
onChange={(e) => setRiderForm({ ...riderForm, shiftid: Number(e.target.value) })}
|
|
className="w-full border border-slate-250 rounded-xl p-3 bg-white outline-none focus:border-purple-500 transition-all font-semibold text-xs text-slate-800 cursor-pointer"
|
|
>
|
|
<option value={0}>Select Shift timings...</option>
|
|
{shiftsQ.isLoading ? (
|
|
<option>Loading shifts...</option>
|
|
) : (shiftsQ.data || []).map((sh: any) => (
|
|
<option key={sh.shiftid} value={fnum(sh.shiftid)}>{fstr(sh.shiftname)}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] font-bold text-slate-450 uppercase tracking-widest">Vehicle Name</label>
|
|
<input
|
|
type="text"
|
|
placeholder="e.g. Coimbatore Hub Bike 4"
|
|
value={riderForm.vehiclename}
|
|
onChange={(e) => setRiderForm({ ...riderForm, vehiclename: e.target.value })}
|
|
className="w-full border border-slate-250 rounded-xl p-3 bg-white outline-none focus:border-purple-500 transition-all font-semibold text-xs text-slate-800"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] font-bold text-slate-450 uppercase tracking-widest">Vehicle Model</label>
|
|
<input
|
|
type="text"
|
|
placeholder="e.g. TVS iQube Electric"
|
|
value={riderForm.vehiclemodel}
|
|
onChange={(e) => setRiderForm({ ...riderForm, vehiclemodel: e.target.value })}
|
|
className="w-full border border-slate-250 rounded-xl p-3 bg-white outline-none focus:border-purple-500 transition-all font-semibold text-xs text-slate-800"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-1 md:col-span-2">
|
|
<label className="text-[10px] font-bold text-slate-450 uppercase tracking-widest">License Number</label>
|
|
<input
|
|
type="text"
|
|
placeholder="e.g. TN-37-BY-1234"
|
|
value={riderForm.licensenumber}
|
|
onChange={(e) => setRiderForm({ ...riderForm, licensenumber: e.target.value })}
|
|
className="w-full border border-slate-250 rounded-xl p-3 bg-white outline-none focus:border-purple-500 transition-all font-semibold text-xs text-slate-800"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-end pt-4">
|
|
<button
|
|
type="submit"
|
|
disabled={createUserMut.isPending}
|
|
className="bg-purple-650 hover:bg-purple-755 text-white px-6 py-3 rounded-xl text-xs font-bold uppercase tracking-wider flex items-center gap-2 cursor-pointer shadow-sm active:scale-95 transition-all border-none"
|
|
>
|
|
{createUserMut.isPending ? (
|
|
<>
|
|
<Loader2 size={14} className="animate-spin" />
|
|
Registering User Profile...
|
|
</>
|
|
) : (
|
|
<>
|
|
<Check size={14} />
|
|
Onboard Rider Account
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|