dispatch page
This commit is contained in:
@@ -9,27 +9,29 @@
|
||||
* Self-contained: search box, role filter, live query, and Add User modal.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Users,
|
||||
Search,
|
||||
X,
|
||||
Plus,
|
||||
ShieldAlert,
|
||||
Shield,
|
||||
User,
|
||||
Mail,
|
||||
Phone,
|
||||
MapPin,
|
||||
Lock,
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Users,
|
||||
Search,
|
||||
X,
|
||||
Plus,
|
||||
ShieldAlert,
|
||||
Shield,
|
||||
User,
|
||||
Mail,
|
||||
Phone,
|
||||
MapPin,
|
||||
UserCheck,
|
||||
Check,
|
||||
SlidersHorizontal,
|
||||
Coins
|
||||
Coins,
|
||||
Store,
|
||||
Bike
|
||||
} from 'lucide-react';
|
||||
import { useFiestaUsers, useFiestaCreateUser } from '../services/fiestaQueries';
|
||||
import { useFiestaUsers, useFiestaCreateUser, useFiestaRiderShifts, useFiestaTenantLocations } from '../services/fiestaQueries';
|
||||
import { useAppRoles } from '../services/queries';
|
||||
import { FIESTA_TENANT_ID, str as fstr, num as fnum, roleName } from '../services/fiestaApi';
|
||||
import AddressAutocomplete, { type AddressResult } from './AddressAutocomplete';
|
||||
|
||||
interface UsersPanelProps {
|
||||
tenantId?: number;
|
||||
@@ -45,7 +47,7 @@ const USER_AVATARS = [
|
||||
|
||||
const ROLE_THEMES: Record<number, { bg: string; text: string; border: string; label: string }> = {
|
||||
1: { bg: 'bg-rose-50/75', text: 'text-rose-700', border: 'border-rose-100', label: 'Owner' },
|
||||
2: { bg: 'bg-amber-50/75', text: 'text-amber-700', border: 'border-amber-100', label: 'Manager' },
|
||||
2: { bg: 'bg-amber-50/75', text: 'text-amber-700', border: 'border-amber-100', label: 'Admin' },
|
||||
3: { bg: 'bg-blue-50/75', text: 'text-blue-700', border: 'border-blue-100', label: 'Admin' },
|
||||
4: { bg: 'bg-emerald-50/75', text: 'text-emerald-700', border: 'border-emerald-100', label: 'Staff' },
|
||||
6: { bg: 'bg-indigo-50/75', text: 'text-indigo-700', border: 'border-indigo-100', label: 'Cashier' },
|
||||
@@ -63,7 +65,7 @@ const ROLE_META: Record<number, { icon: typeof ShieldAlert; desc: string }> = {
|
||||
/** Fallback role choices when the app-roles API returns nothing. */
|
||||
const FALLBACK_ROLE_CHOICES = [
|
||||
{ id: 1, label: 'Owner', desc: 'Full business access', icon: ShieldAlert },
|
||||
{ id: 2, label: 'Manager', desc: 'Operations control', icon: Shield },
|
||||
{ id: 2, label: 'Admin', desc: 'Operations control', icon: Shield },
|
||||
{ id: 3, label: 'Admin', desc: 'Manage store settings', icon: SlidersHorizontal },
|
||||
{ id: 4, label: 'Staff', desc: 'Standard staff duties', icon: User },
|
||||
{ id: 6, label: 'Cashier', desc: 'Checkout & registers', icon: Coins },
|
||||
@@ -76,34 +78,81 @@ export default function UsersPanel({ tenantId = FIESTA_TENANT_ID, defaultNewUser
|
||||
|
||||
// Selectable roles for the Add User modal — driven by the live app-roles API,
|
||||
// matched to local icon/desc styling by roleid; falls back to the static list.
|
||||
// Selectable roles for the Add User modal - limited to Staff and Rider.
|
||||
const roleChoices = React.useMemo(() => {
|
||||
const rows = rolesQ.data ?? [];
|
||||
const mapped = rows
|
||||
.map((r) => {
|
||||
const id = fnum((r as Record<string, unknown>).roleid);
|
||||
const label =
|
||||
fstr((r as Record<string, unknown>).rolename) ||
|
||||
fstr((r as Record<string, unknown>).name) ||
|
||||
roleName(id);
|
||||
const meta = ROLE_META[id];
|
||||
return { id, label, desc: meta?.desc ?? '', icon: meta?.icon ?? User };
|
||||
})
|
||||
.filter((r) => r.id > 0);
|
||||
return mapped.length ? mapped : FALLBACK_ROLE_CHOICES;
|
||||
}, [rolesQ.data]);
|
||||
return [
|
||||
{ id: 4, label: 'Staff', desc: 'Standard store staff duties', icon: User, configid: 15 },
|
||||
{ id: 5, label: 'Rider', desc: 'Delivery fleet rider', icon: Bike, configid: 6 },
|
||||
];
|
||||
}, []);
|
||||
|
||||
const [search, setSearch] = useState('');
|
||||
const [userRoleFilter, setUserRoleFilter] = useState<number | 'ALL'>('ALL');
|
||||
const [userRoleFilter, setUserRoleFilter] = useState<string | 'ALL'>('ALL');
|
||||
const [showAddUserModal, setShowAddUserModal] = useState(false);
|
||||
const [newUser, setNewUser] = useState({
|
||||
firstname: '',
|
||||
lastname: '',
|
||||
email: '',
|
||||
contactno: '',
|
||||
password: '',
|
||||
roleid: defaultNewUserRole,
|
||||
locationid: 0,
|
||||
applocationid: 0,
|
||||
address: '',
|
||||
suburb: '',
|
||||
city: '',
|
||||
state: '',
|
||||
postcode: '',
|
||||
latitude: '',
|
||||
longitude: '',
|
||||
shiftid: 0,
|
||||
});
|
||||
|
||||
// Stores/branches for this merchant — the new user is bound to the store the
|
||||
// admin picks (its locationid + applocationid go into the create payload).
|
||||
const locationsQ = useFiestaTenantLocations(tenantId);
|
||||
const allStores = (locationsQ.data ?? [])
|
||||
.map((l) => ({
|
||||
locationid: fnum((l as Record<string, unknown>).locationid),
|
||||
applocationid: fnum((l as Record<string, unknown>).applocationid) || 1,
|
||||
name: fstr((l as Record<string, unknown>).locationname) || `Store ${fnum((l as Record<string, unknown>).locationid)}`,
|
||||
address: fstr((l as Record<string, unknown>).address),
|
||||
status: fstr((l as Record<string, unknown>).status),
|
||||
}))
|
||||
.filter((s) => s.locationid > 0);
|
||||
// Prefer Active stores (per the create-staff flow); fall back to all if a tenant
|
||||
// doesn't flag status, so the picker is never empty.
|
||||
const activeStores = allStores.filter((s) => s.status.toLowerCase() === 'active');
|
||||
const storeOptions = activeStores.length ? activeStores : allStores;
|
||||
|
||||
// Auto-bind when the merchant has exactly one store (nothing to choose).
|
||||
useEffect(() => {
|
||||
if (!newUser.locationid && storeOptions.length === 1) {
|
||||
setNewUser((u) => ({ ...u, locationid: storeOptions[0].locationid, applocationid: storeOptions[0].applocationid }));
|
||||
}
|
||||
}, [storeOptions.length, newUser.locationid]);
|
||||
|
||||
// Rider-shift picker — shown only when a rider role is selected (parity with the
|
||||
// merchant_web create form). Shifts come from the live partners/getridershifts.
|
||||
const selectedRole = roleChoices.find((r) => r.id === newUser.roleid);
|
||||
const isRiderRole = (selectedRole?.label || '').toLowerCase().includes('rider') || newUser.roleid === 5;
|
||||
const shiftsQ = useFiestaRiderShifts();
|
||||
const shiftOptions = (shiftsQ.data ?? [])
|
||||
.map((s) => ({ id: fnum((s as Record<string, unknown>).shiftid), label: fstr((s as Record<string, unknown>).shiftname) || `Shift ${fnum((s as Record<string, unknown>).shiftid)}` }))
|
||||
.filter((s) => s.id > 0);
|
||||
|
||||
// Address autocomplete → discrete fields (or clear them when the field is emptied).
|
||||
const handleAddressSelect = (r: AddressResult | null) => {
|
||||
setNewUser((u) => ({
|
||||
...u,
|
||||
address: r?.address ?? '',
|
||||
suburb: r?.suburb ?? '',
|
||||
city: r?.city ?? '',
|
||||
state: r?.state ?? '',
|
||||
postcode: r?.postcode ?? '',
|
||||
latitude: r?.latitude ?? '',
|
||||
longitude: r?.longitude ?? '',
|
||||
}));
|
||||
};
|
||||
|
||||
// Live users mapped to display rows (rendered directly from the query).
|
||||
const users = (usersQ.data ?? []).map((u, i) => {
|
||||
const shift = fstr(u.shiftname).trim();
|
||||
@@ -117,7 +166,7 @@ export default function UsersPanel({ tenantId = FIESTA_TENANT_ID, defaultNewUser
|
||||
email: fstr(u.email) || fstr(u.authname) || '—',
|
||||
contact: fstr(u.contactno) || '—',
|
||||
roleid: Number(u.roleid),
|
||||
role: roleName(Number(u.roleid)),
|
||||
role: roleName(Number(u.roleid)) === 'Manager' ? 'Admin' : roleName(Number(u.roleid)),
|
||||
shift: shift && shift !== '-' ? shift : '—',
|
||||
location: fstr(u.applocation) || fstr(u.city) || 'Coimbatore',
|
||||
status: fstr(u.status) || 'Active',
|
||||
@@ -132,30 +181,59 @@ export default function UsersPanel({ tenantId = FIESTA_TENANT_ID, defaultNewUser
|
||||
u.name.toLowerCase().includes(q) ||
|
||||
u.email.toLowerCase().includes(q) ||
|
||||
u.contact.toLowerCase().includes(q);
|
||||
const matchesRole = userRoleFilter === 'ALL' || u.roleid === userRoleFilter;
|
||||
const matchesRole = userRoleFilter === 'ALL' || u.role === userRoleFilter;
|
||||
return matchesSearch && matchesRole;
|
||||
});
|
||||
|
||||
const roleOptions = Array.from(new Set(users.map((u) => u.roleid)));
|
||||
const roleOptions = React.useMemo(() => {
|
||||
return Array.from(new Set(users.map((u) => u.role)));
|
||||
}, [users]);
|
||||
|
||||
const handleCreateUser = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!newUser.firstname || !newUser.email || !newUser.contactno || !newUser.password) {
|
||||
alert('Please provide first name, email, contact number, and a password.');
|
||||
if (!newUser.firstname || !newUser.email || !newUser.contactno) {
|
||||
alert('Please provide first name, contact number, and email.');
|
||||
return;
|
||||
}
|
||||
if (!/^\d{10}$/.test(newUser.contactno.trim())) {
|
||||
alert('Contact number must be exactly 10 digits.');
|
||||
return;
|
||||
}
|
||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(newUser.email.trim())) {
|
||||
alert('Please enter a valid email address.');
|
||||
return;
|
||||
}
|
||||
if (!Number(newUser.roleid)) {
|
||||
alert('Please select a role.');
|
||||
return;
|
||||
}
|
||||
if (!newUser.locationid) {
|
||||
alert('Please select the store this user belongs to.');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await createUserMut.mutateAsync({
|
||||
firstname: newUser.firstname,
|
||||
lastname: newUser.lastname,
|
||||
email: newUser.email,
|
||||
contactno: newUser.contactno,
|
||||
password: newUser.password,
|
||||
roleid: Number(newUser.roleid),
|
||||
configid: selectedRole?.configid ?? 15,
|
||||
// Store binding — sent exactly as the backend expects so the user is tied
|
||||
// to the chosen branch (tenantid + locationid + applocationid).
|
||||
tenantid: tenantId,
|
||||
locationid: newUser.locationid,
|
||||
applocationid: newUser.applocationid || 1,
|
||||
address: newUser.address,
|
||||
suburb: newUser.suburb,
|
||||
city: newUser.city,
|
||||
state: newUser.state,
|
||||
postcode: newUser.postcode,
|
||||
latitude: newUser.latitude,
|
||||
longitude: newUser.longitude,
|
||||
shiftid: isRiderRole ? Number(newUser.shiftid) || 0 : 0,
|
||||
});
|
||||
setShowAddUserModal(false);
|
||||
setNewUser({ firstname: '', lastname: '', email: '', contactno: '', password: '', roleid: defaultNewUserRole });
|
||||
setNewUser({ firstname: '', email: '', contactno: '', roleid: defaultNewUserRole, locationid: 0, applocationid: 0, address: '', suburb: '', city: '', state: '', postcode: '', latitude: '', longitude: '', shiftid: 0 });
|
||||
alert(`Team member "${newUser.firstname}" added successfully.`);
|
||||
} catch (err) {
|
||||
alert(`Could not add team member: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
||||
@@ -199,25 +277,27 @@ export default function UsersPanel({ tenantId = FIESTA_TENANT_ID, defaultNewUser
|
||||
|
||||
{/* Search & Filter Utility Bar */}
|
||||
<div className="bg-slate-50/50 border border-slate-200/60 p-4 rounded-2xl flex flex-col md:flex-row gap-4 items-stretch md:items-center justify-between select-none">
|
||||
<div className="relative w-full md:max-w-sm shrink-0">
|
||||
<span className="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none text-slate-450">
|
||||
<Search className="w-4.5 h-4.5" />
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search team…"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="w-full pl-10 pr-9 py-2.5 bg-white border border-slate-200/80 rounded-xl text-sm font-medium text-slate-800 placeholder-slate-405 focus:outline-none focus:ring-4 focus:ring-purple-500/10 focus:border-purple-500 transition-all shadow-sm"
|
||||
/>
|
||||
{search && (
|
||||
<button
|
||||
onClick={() => setSearch('')}
|
||||
className="absolute inset-y-0 right-0 pr-3 flex items-center text-slate-450 hover:text-slate-700"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
<div className="relative w-full md:max-w-md shrink-0 group">
|
||||
<div className="relative flex items-center bg-white border border-slate-200 rounded-xl transition-all duration-300 shadow-sm focus-within:ring-4 focus-within:ring-purple-150 focus-within:border-purple-600 hover:border-slate-300">
|
||||
<span className="pl-4 flex items-center pointer-events-none text-slate-400 group-focus-within:text-purple-600 transition-colors">
|
||||
<Search className="w-4.5 h-4.5" />
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search team members by name, email, phone..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="w-full pl-3 pr-10 py-3 bg-transparent border-none text-xs font-semibold text-slate-800 placeholder-slate-400 focus:outline-none outline-none"
|
||||
/>
|
||||
{search && (
|
||||
<button
|
||||
onClick={() => setSearch('')}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 active:scale-95 transition-all p-1 hover:bg-slate-100 rounded-lg"
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Role filter capsules */}
|
||||
@@ -232,17 +312,17 @@ export default function UsersPanel({ tenantId = FIESTA_TENANT_ID, defaultNewUser
|
||||
>
|
||||
All Roles
|
||||
</button>
|
||||
{roleOptions.map((rid) => (
|
||||
{roleOptions.map((roleNameStr) => (
|
||||
<button
|
||||
key={rid}
|
||||
onClick={() => setUserRoleFilter(rid)}
|
||||
key={roleNameStr}
|
||||
onClick={() => setUserRoleFilter(roleNameStr)}
|
||||
className={`px-3.5 py-2 rounded-xl text-xs uppercase tracking-wider font-extrabold transition-all duration-200 cursor-pointer border whitespace-nowrap ${
|
||||
userRoleFilter === rid
|
||||
userRoleFilter === roleNameStr
|
||||
? 'bg-purple-600 text-white border-purple-600 shadow-sm'
|
||||
: 'bg-white text-slate-600 border-slate-200/80 hover:text-slate-800 hover:bg-slate-100'
|
||||
}`}
|
||||
>
|
||||
{roleName(rid)}
|
||||
{roleNameStr}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -352,34 +432,42 @@ export default function UsersPanel({ tenantId = FIESTA_TENANT_ID, defaultNewUser
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Name Fields */}
|
||||
<div className="grid grid-cols-2 gap-sm">
|
||||
<div className="space-y-1.5">
|
||||
<label className="font-bold text-slate-500 uppercase tracking-widest text-[10px]">FIRST NAME (*)</label>
|
||||
{/* Firstname */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="font-bold text-slate-500 uppercase tracking-widest text-[10px]">FIRSTNAME (*)</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="e.g. Harini"
|
||||
value={newUser.firstname}
|
||||
onChange={(e) => setNewUser({ ...newUser, firstname: e.target.value })}
|
||||
className="w-full border border-slate-200 rounded-xl p-3 bg-slate-50/40 hover:bg-slate-100 focus:bg-white outline-none focus:border-purple-500 focus:ring-4 focus:ring-purple-500/10 transition-all text-slate-800 font-semibold text-sm shadow-sm"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Contactno */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="font-bold text-slate-500 uppercase tracking-widest text-[10px]">CONTACTNO (*)</label>
|
||||
<div className="relative">
|
||||
<span className="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none text-slate-400">
|
||||
<Phone size={14} />
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="e.g. Harini"
|
||||
value={newUser.firstname}
|
||||
onChange={(e) => setNewUser({ ...newUser, firstname: e.target.value })}
|
||||
className="w-full border border-slate-200 rounded-xl p-3 bg-slate-50/40 hover:bg-slate-100 focus:bg-white outline-none focus:border-purple-500 focus:ring-4 focus:ring-purple-500/10 transition-all text-slate-800 font-semibold text-sm shadow-sm"
|
||||
inputMode="numeric"
|
||||
maxLength={10}
|
||||
placeholder="e.g. 9988776655"
|
||||
value={newUser.contactno}
|
||||
onChange={(e) => setNewUser({ ...newUser, contactno: e.target.value.replace(/\D/g, '') })}
|
||||
className="w-full border border-slate-200 rounded-xl pl-10 pr-4 py-3 bg-slate-50/40 hover:bg-slate-100 focus:bg-white outline-none focus:border-purple-500 focus:ring-4 focus:ring-purple-500/10 transition-all text-slate-800 font-semibold text-sm shadow-sm"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="font-bold text-slate-500 uppercase tracking-widest text-[10px]">LAST NAME</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="e.g. Rajan"
|
||||
value={newUser.lastname}
|
||||
onChange={(e) => setNewUser({ ...newUser, lastname: e.target.value })}
|
||||
className="w-full border border-slate-200 rounded-xl p-3 bg-slate-50/40 hover:bg-slate-100 focus:bg-white outline-none focus:border-purple-500 focus:ring-4 focus:ring-purple-500/10 transition-all text-slate-800 font-semibold text-sm shadow-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Email & Contact */}
|
||||
{/* Email */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="font-bold text-slate-500 uppercase tracking-widest text-[10px]">EMAIL ADDRESS (*)</label>
|
||||
<label className="font-bold text-slate-500 uppercase tracking-widest text-[10px]">EMAIL (*)</label>
|
||||
<div className="relative">
|
||||
<span className="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none text-slate-400">
|
||||
<Mail size={14} />
|
||||
@@ -395,23 +483,6 @@ export default function UsersPanel({ tenantId = FIESTA_TENANT_ID, defaultNewUser
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label className="font-bold text-slate-500 uppercase tracking-widest text-[10px]">CONTACT NUMBER (*)</label>
|
||||
<div className="relative">
|
||||
<span className="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none text-slate-400">
|
||||
<Phone size={14} />
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="e.g. 9988776655"
|
||||
value={newUser.contactno}
|
||||
onChange={(e) => setNewUser({ ...newUser, contactno: e.target.value })}
|
||||
className="w-full border border-slate-200 rounded-xl pl-10 pr-4 py-3 bg-slate-50/40 hover:bg-slate-100 focus:bg-white outline-none focus:border-purple-500 focus:ring-4 focus:ring-purple-500/10 transition-all text-slate-800 font-semibold text-sm shadow-sm"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Interactive Role Buttons instead of standard select */}
|
||||
<div className="space-y-2">
|
||||
<label className="font-bold text-slate-500 uppercase tracking-widest text-[10px]">SELECT ACCOUNT ROLE (*)</label>
|
||||
@@ -441,23 +512,70 @@ export default function UsersPanel({ tenantId = FIESTA_TENANT_ID, defaultNewUser
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Temporary Password */}
|
||||
{/* Store / branch — binds the user to a specific outlet (tenantid + locationid + applocationid) */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="font-bold text-slate-500 uppercase tracking-widest text-[10px]">TEMPORARY PASSWORD (*)</label>
|
||||
<label className="font-bold text-slate-500 uppercase tracking-widest text-[10px]">STORE / BRANCH (*)</label>
|
||||
<div className="relative">
|
||||
<span className="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none text-slate-400">
|
||||
<Lock size={14} />
|
||||
<Store size={14} />
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Set password credentials"
|
||||
value={newUser.password}
|
||||
onChange={(e) => setNewUser({ ...newUser, password: e.target.value })}
|
||||
className="w-full border border-slate-200 rounded-xl pl-10 pr-4 py-3 bg-slate-50/40 hover:bg-slate-100 focus:bg-white outline-none focus:border-purple-500 focus:ring-4 focus:ring-purple-500/10 transition-all text-slate-800 font-semibold font-mono text-sm shadow-sm"
|
||||
required
|
||||
/>
|
||||
<select
|
||||
value={newUser.locationid}
|
||||
onChange={(e) => {
|
||||
const lid = Number(e.target.value);
|
||||
const s = storeOptions.find((o) => o.locationid === lid);
|
||||
setNewUser({ ...newUser, locationid: lid, applocationid: s?.applocationid ?? 0 });
|
||||
}}
|
||||
className="w-full border border-slate-200 rounded-xl pl-10 pr-4 py-3 bg-slate-50/40 hover:bg-slate-100 focus:bg-white outline-none focus:border-purple-500 focus:ring-4 focus:ring-purple-500/10 transition-all text-slate-800 font-semibold text-sm shadow-sm cursor-pointer"
|
||||
>
|
||||
<option value={0}>{locationsQ.isLoading ? 'Loading stores…' : storeOptions.length ? 'Select a store…' : 'No stores found'}</option>
|
||||
{storeOptions.map((s) => (
|
||||
<option key={s.locationid} value={s.locationid}>{s.name}{s.address ? ` — ${s.address}` : ''}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Rider shift — only when a rider role is selected */}
|
||||
{isRiderRole && (
|
||||
<div className="space-y-1.5 animate-in slide-in-from-top duration-250">
|
||||
<label className="font-bold text-slate-500 uppercase tracking-widest text-[10px]">RIDER SHIFT</label>
|
||||
<div className="relative">
|
||||
<span className="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none text-slate-400">
|
||||
<Clock size={14} />
|
||||
</span>
|
||||
<select
|
||||
value={newUser.shiftid}
|
||||
onChange={(e) => setNewUser({ ...newUser, shiftid: Number(e.target.value) })}
|
||||
className="w-full border border-slate-200 rounded-xl pl-10 pr-4 py-3 bg-slate-50/40 hover:bg-slate-100 focus:bg-white outline-none focus:border-purple-500 focus:ring-4 focus:ring-purple-500/10 transition-all text-slate-800 font-semibold text-sm shadow-sm cursor-pointer"
|
||||
>
|
||||
<option value={0}>{shiftsQ.isLoading ? 'Loading shifts…' : shiftOptions.length ? 'Select rider shift…' : 'No shifts available'}</option>
|
||||
{shiftOptions.map((s) => <option key={s.id} value={s.id}>{s.label}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Address — keyless autocomplete that fills suburb / city / state / postcode */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="font-bold text-slate-500 uppercase tracking-widest text-[10px]">ADDRESS</label>
|
||||
<AddressAutocomplete value={newUser.address} onSelect={handleAddressSelect} placeholder="Search address…" />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-sm">
|
||||
{(['suburb', 'city', 'state', 'postcode'] as const).map((key) => (
|
||||
<div className="space-y-1.5" key={key}>
|
||||
<label className="font-bold text-slate-500 uppercase tracking-widest text-[10px]">{key.toUpperCase()}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newUser[key]}
|
||||
onChange={(e) => setNewUser({ ...newUser, [key]: e.target.value })}
|
||||
className="w-full border border-slate-200 rounded-xl p-3 bg-slate-50/40 hover:bg-slate-100 focus:bg-white outline-none focus:border-purple-500 focus:ring-4 focus:ring-purple-500/10 transition-all text-slate-800 font-semibold text-sm shadow-sm"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user