changes on global catalogue

This commit is contained in:
2026-06-20 18:45:25 +05:30
parent 2e9ffb15bb
commit 8707004405
22 changed files with 3137 additions and 2070 deletions

View File

@@ -29,7 +29,7 @@ import {
Bike,
Clock
} from 'lucide-react';
import { useFiestaUsers, useFiestaCreateUser, useFiestaRiderShifts, useFiestaTenantLocations } from '../services/fiestaQueries';
import { useFiestaUsers, useFiestaCreateUser, useFiestaUpdateUser, 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';
@@ -76,8 +76,30 @@ const FALLBACK_ROLE_CHOICES = [
export default function UsersPanel({ tenantId = FIESTA_TENANT_ID, defaultNewUserRole = 4 }: UsersPanelProps) {
const usersQ = useFiestaUsers({ tenantid: tenantId, pagesize: 100 });
const createUserMut = useFiestaCreateUser();
const updateUserMut = useFiestaUpdateUser();
const rolesQ = useAppRoles();
const [assignUserId, setAssignUserId] = useState<number | null>(null);
const [assignLocationId, setAssignLocationId] = useState<number>(0);
const [assignRoleId, setAssignRoleId] = useState<number>(4);
const handleToggleAssign = async (userid: number, currentRoleId: number) => {
try {
if (currentRoleId <= 0) {
// Open the assign modal
setAssignUserId(userid);
if (storeOptions.length === 1) {
setAssignLocationId(storeOptions[0].locationid);
}
} else {
// Unassign immediately. We use -1 because the Go backend ignores 0 (zero value) during updates.
await updateUserMut.mutateAsync({ userid, roleid: -1, locationid: -1, applocationid: -1 });
}
} catch (err) {
alert(`Could not update user role: ${err instanceof Error ? err.message : 'Unknown error'}`);
}
};
// 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.
@@ -130,7 +152,10 @@ export default function UsersPanel({ tenantId = FIESTA_TENANT_ID, defaultNewUser
if (!newUser.locationid && storeOptions.length === 1) {
setNewUser((u) => ({ ...u, locationid: storeOptions[0].locationid, applocationid: storeOptions[0].applocationid }));
}
}, [storeOptions.length, newUser.locationid]);
if (!assignLocationId && storeOptions.length === 1) {
setAssignLocationId(storeOptions[0].locationid);
}
}, [storeOptions.length, newUser.locationid, assignLocationId]);
// 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.
@@ -189,6 +214,7 @@ export default function UsersPanel({ tenantId = FIESTA_TENANT_ID, defaultNewUser
const roleOptions = React.useMemo(() => {
const roles = new Set(users.map((u) => u.role));
roles.add('Admin');
roles.add('Rider');
return Array.from(roles);
}, [users]);
@@ -280,25 +306,25 @@ export default function UsersPanel({ tenantId = FIESTA_TENANT_ID, defaultNewUser
</div>
{/* Search & Filter Utility Bar */}
<div className="bg-white border border-slate-200/80 p-3 rounded-2xl flex flex-col md:flex-row gap-4 items-stretch md:items-center justify-between shadow-sm">
<div className="relative w-full md:max-w-md shrink-0">
<div className="relative flex w-full items-center bg-slate-50 border border-slate-200/80 rounded-xl transition-all duration-300 focus-within:bg-white focus-within:ring-4 focus-within:ring-purple-500/10 focus-within:border-purple-500 group-search shadow-inner">
<span className="pl-4 flex items-center pointer-events-none text-slate-400 transition-colors">
<Search size={18} strokeWidth={2.5} />
<div className="flex flex-col md:flex-row gap-6 items-stretch md:items-center justify-between mb-4">
<div className="relative w-full md:max-w-sm shrink-0">
<div className="relative flex w-full items-center border-b-2 border-slate-200 focus-within:border-purple-600 transition-colors duration-300">
<span className="pl-1 flex items-center pointer-events-none text-slate-400">
<Search size={16} strokeWidth={3} />
</span>
<input
type="text"
placeholder="Search team members..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-full pl-3 pr-12 py-3 bg-transparent border-none text-sm font-semibold text-slate-800 placeholder-slate-400 focus:outline-none outline-none"
className="w-full pl-3 pr-10 py-2.5 bg-transparent border-none text-sm 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-700 hover:bg-slate-200/50 active:scale-95 transition-all p-1.5 rounded-lg flex items-center justify-center"
className="absolute right-1 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-700 active:scale-95 transition-all p-1.5 flex items-center justify-center"
>
<X size={16} strokeWidth={2.5} />
<X size={14} strokeWidth={3} />
</button>
)}
</div>
@@ -333,7 +359,7 @@ export default function UsersPanel({ tenantId = FIESTA_TENANT_ID, defaultNewUser
</div>
{/* Directory Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-md">
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
{filteredUsers.length === 0 ? (
<div className="col-span-full bg-white border border-slate-200/60 rounded-2xl p-12 text-center text-slate-500 font-bold text-base">
{usersQ.isLoading ? 'Loading team list…' : 'No matches found in team list.'}
@@ -342,62 +368,77 @@ export default function UsersPanel({ tenantId = FIESTA_TENANT_ID, defaultNewUser
filteredUsers.map((u) => {
const roleInfo = ROLE_THEMES[u.roleid] || { bg: 'bg-slate-55', text: 'text-slate-700', border: 'border-slate-100', label: u.role };
return (
<div key={u.userid} className="bg-white border border-slate-200/60 hover:border-purple-300 hover:shadow-md rounded-2xl p-5 transition-all duration-300 flex flex-col justify-between group relative overflow-hidden">
<div key={u.userid} className="bg-white border border-slate-200/60 hover:border-purple-300 hover:shadow-md rounded-2xl p-4 transition-all duration-300 flex flex-col justify-between group relative overflow-hidden">
{/* Background aura gradient effect on hover */}
<div className="absolute top-0 right-0 w-28 h-28 bg-purple-500/5 rounded-full blur-xl -mr-8 -mt-8 pointer-events-none transition-all group-hover:bg-purple-500/10" />
<div className="absolute top-0 right-0 w-24 h-24 bg-purple-500/5 rounded-full blur-xl -mr-6 -mt-6 pointer-events-none transition-all group-hover:bg-purple-500/10" />
<div>
<div className="flex items-start gap-4 relative z-10">
<div className="flex items-start gap-3 relative z-10">
{/* User Avatar with status indicator ring */}
<div className="relative shrink-0 select-none">
<img
src={u.avatar}
alt={u.name}
referrerPolicy="no-referrer"
className="w-14 h-14 object-cover rounded-2xl border border-slate-200"
className="w-10 h-10 object-cover rounded-[10px] border border-slate-200"
/>
<span className={`absolute -bottom-1 -right-1 w-4 h-4 rounded-full border-3 border-white ${
<span className={`absolute -bottom-1 -right-1 w-3.5 h-3.5 rounded-full border-[2.5px] border-white ${
u.status.toLowerCase() === 'active' ? 'bg-emerald-500' : 'bg-slate-350'
}`} />
</div>
<div className="min-w-0 flex-1">
<h4 className="font-bold text-slate-800 text-sm truncate group-hover:text-purple-950 transition-colors leading-tight">{u.name}</h4>
<p className="text-xs text-slate-450 mt-1.5 truncate font-medium">{u.email}</p>
<p className="text-[11px] text-slate-450 mt-1 truncate font-medium">{u.email}</p>
</div>
</div>
{/* Metadata fields */}
<div className="mt-5 space-y-2.5 border-t border-slate-100/75 pt-4 text-xs">
<div className="flex items-center gap-2.5 text-slate-600 font-medium">
<Phone size={14} className="text-slate-400 shrink-0" />
<div className="mt-4 space-y-2 border-t border-slate-100/75 pt-3 text-[11px]">
<div className="flex items-center gap-2 text-slate-600 font-medium">
<Phone size={13} className="text-slate-400 shrink-0" />
<span className="font-mono">{u.contact}</span>
</div>
<div className="flex items-center gap-2.5 text-slate-600 font-medium">
<MapPin size={14} className="text-slate-400 shrink-0" />
<div className="flex items-center gap-2 text-slate-600 font-medium">
<MapPin size={13} className="text-slate-400 shrink-0" />
<span className="truncate">{u.location}</span>
</div>
{u.shift && u.shift !== '—' && (
<div className="flex items-center gap-2.5 text-slate-600 font-medium">
<span className="text-[10px] uppercase tracking-wider text-slate-400 font-extrabold">Shift</span>
<span className="font-bold text-slate-700 bg-slate-50 px-2.5 py-0.5 rounded-lg border border-slate-150 text-xs">{u.shift}</span>
<div className="flex items-center gap-2 text-slate-600 font-medium">
<span className="text-[9px] uppercase tracking-wider text-slate-400 font-extrabold">Shift</span>
<span className="font-bold text-slate-700 bg-slate-50 px-2 py-0.5 rounded-md border border-slate-150">{u.shift}</span>
</div>
)}
</div>
</div>
<div className="mt-5 pt-4 border-t border-slate-100/75 flex justify-between items-center select-none">
<span className={`inline-flex items-center gap-1.5 px-3 py-1 rounded-xl text-xs font-extrabold uppercase border ${roleInfo.bg} ${roleInfo.text} ${roleInfo.border}`}>
{u.roleid === 1 && <ShieldAlert size={12} />}
{u.roleid === 2 && <Shield size={12} />}
{u.roleid === 3 && <SlidersHorizontal size={12} />}
{u.roleid === 4 && <User size={12} />}
{u.roleid === 5 && <Bike size={12} />}
{u.roleid === 6 && <Coins size={12} />}
{roleInfo.label}
</span>
<div className="mt-4 pt-3 border-t border-slate-100/75 flex justify-between items-center select-none">
<div className="flex items-center gap-2">
<span className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-[8px] text-[10px] font-extrabold uppercase border ${roleInfo.bg} ${roleInfo.text} ${roleInfo.border}`}>
{u.roleid === 1 && <ShieldAlert size={10} />}
{u.roleid === 2 && <Shield size={10} />}
{u.roleid === 3 && <SlidersHorizontal size={10} />}
{u.roleid === 4 && <User size={10} />}
{u.roleid === 5 && <Bike size={10} />}
{u.roleid === 6 && <Coins size={10} />}
{roleInfo.label}
</span>
{u.roleid !== 1 && (
<button
onClick={() => handleToggleAssign(u.userid, u.roleid)}
disabled={updateUserMut.isPending}
className={`px-2 py-0.5 rounded-[8px] text-[9px] font-extrabold uppercase transition-colors cursor-pointer border ${
u.roleid <= 0
? 'bg-emerald-50 text-emerald-700 border-emerald-200 hover:bg-emerald-100'
: 'bg-slate-50 text-slate-500 border-slate-200 hover:bg-slate-100 hover:text-slate-700'
} ${updateUserMut.isPending ? 'opacity-50 cursor-not-allowed' : ''}`}
>
{u.roleid <= 0 ? 'Assign' : 'Unassign'}
</button>
)}
</div>
<span className="text-xs font-mono text-slate-400 font-bold">ID: #{u.userid}</span>
<span className="text-[10px] font-mono text-slate-400 font-bold">#{u.userid}</span>
</div>
</div>
);
@@ -615,6 +656,99 @@ export default function UsersPanel({ tenantId = FIESTA_TENANT_ID, defaultNewUser
</div>
</div>
)}
{/* ASSIGN USER MODAL */}
{assignUserId !== null && (
<div
className="fixed inset-0 bg-slate-900/40 backdrop-blur-sm z-[200] flex items-center justify-center p-md select-none"
onClick={(e) => { if (e.target === e.currentTarget) setAssignUserId(null); }}
>
<div className="bg-white border border-slate-200/80 rounded-2xl w-full max-w-[28rem] flex flex-col shadow-2xl overflow-hidden animate-in zoom-in-95 duration-200 text-sm font-sans cursor-default">
<div className="p-5 border-b border-slate-100 bg-slate-50/50 flex justify-between items-center shrink-0">
<h4 className="font-bold text-slate-900 flex items-center gap-2.5 text-base">
<div className="w-8 h-8 rounded-lg bg-emerald-50 text-emerald-650 flex items-center justify-center">
<UserCheck size={16} />
</div>
Assign Team Member
</h4>
<button
onClick={() => setAssignUserId(null)}
className="p-1.5 hover:bg-slate-100 rounded-lg text-slate-400 hover:text-slate-600 cursor-pointer transition-colors"
>
<X size={18} />
</button>
</div>
<form onSubmit={async (e) => {
e.preventDefault();
const store = storeOptions.find(s => s.locationid === assignLocationId);
try {
await updateUserMut.mutateAsync({
userid: assignUserId,
roleid: assignRoleId,
locationid: assignLocationId,
applocationid: store?.applocationid ?? 1
});
setAssignUserId(null);
} catch (err) {
alert(`Could not assign user: ${err instanceof Error ? err.message : 'Unknown error'}`);
}
}} className="p-6 space-y-5">
<div className="space-y-1.5">
<label className="font-bold text-slate-500 uppercase tracking-widest text-[10px]">ROLE (*)</label>
<div className="relative">
<select
value={assignRoleId}
onChange={(e) => setAssignRoleId(Number(e.target.value))}
className="w-full border border-slate-200 rounded-xl px-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"
>
{roleChoices.map((r) => (
<option key={r.id} value={r.id}>{r.label}</option>
))}
</select>
</div>
</div>
<div className="space-y-1.5">
<label className="font-bold text-slate-500 uppercase tracking-widest text-[10px]">STORE / LOCATION (*)</label>
<div className="relative">
<span className="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none text-slate-400">
<Store size={14} />
</span>
<select
value={assignLocationId}
onChange={(e) => setAssignLocationId(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"
required
>
<option value={0} disabled>{locationsQ.isLoading ? 'Loading stores…' : 'Select a store…'}</option>
{storeOptions.map((s) => (
<option key={s.locationid} value={s.locationid}>{s.name}{s.address ? `${s.address}` : ''}</option>
))}
</select>
</div>
</div>
<div className="pt-4 border-t border-slate-100 flex justify-end gap-sm mt-6">
<button
type="button"
onClick={() => setAssignUserId(null)}
className="px-5 py-2.5 border border-slate-200 hover:bg-slate-100 rounded-xl font-bold text-slate-500 cursor-pointer active:scale-95 transition-all text-sm"
>
Cancel
</button>
<button
type="submit"
disabled={updateUserMut.isPending || !assignLocationId}
className="px-6 py-2.5 bg-emerald-600 hover:bg-emerald-700 text-white rounded-xl font-bold cursor-pointer shadow-sm disabled:opacity-60 disabled:cursor-not-allowed active:scale-95 transition-all flex items-center gap-1.5 border-none text-sm"
>
{updateUserMut.isPending ? 'Assigning…' : 'Assign User'}
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
}