new changes
This commit is contained in:
349
src/App.tsx
349
src/App.tsx
@@ -7,7 +7,6 @@ import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Network,
|
||||
Truck,
|
||||
Users,
|
||||
Sliders,
|
||||
Calendar,
|
||||
AlertTriangle,
|
||||
@@ -34,10 +33,8 @@ import { MainSection } from './types';
|
||||
import {
|
||||
useFiestaTenantLocations,
|
||||
useFiestaLocationSummary,
|
||||
useFiestaUsers,
|
||||
useFiestaCreateUser,
|
||||
} from './services/fiestaQueries';
|
||||
import { FIESTA_TENANT_ID, str as fstr, roleName } from './services/fiestaApi';
|
||||
import { FIESTA_TENANT_ID, str as fstr } from './services/fiestaApi';
|
||||
import Sidebar from './components/Sidebar';
|
||||
import Header from './components/Header';
|
||||
import DashboardView from './components/DashboardView';
|
||||
@@ -64,19 +61,10 @@ export default function App() {
|
||||
|
||||
// ── Live data for the secondary sections (Fiesta) ─────────────────────────
|
||||
// Stores ← tenant locations + per-location order summary (seeded into local
|
||||
// state so the "Add Store" handler keeps working). Users come straight from
|
||||
// the live Users API and render directly from the query, with the "Add User"
|
||||
// form posting back through the create-user mutation.
|
||||
// state so the "Add Store" handler keeps working). Users management now lives
|
||||
// under Settings → Users & Access (see UsersPanel).
|
||||
const locationsQ = useFiestaTenantLocations(FIESTA_TENANT_ID);
|
||||
const locSummaryQ = useFiestaLocationSummary(FIESTA_TENANT_ID);
|
||||
const usersQ = useFiestaUsers({ tenantid: FIESTA_TENANT_ID, pagesize: 100 });
|
||||
const createUserMut = useFiestaCreateUser();
|
||||
|
||||
const USER_AVATARS = [
|
||||
'https://images.unsplash.com/photo-1534528741775-53994a69daeb?auto=format&fit=crop&w=150&q=80',
|
||||
'https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&w=150&q=80',
|
||||
'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&w=150&q=80',
|
||||
];
|
||||
|
||||
const STORE_COVERS = [
|
||||
'https://images.unsplash.com/photo-1542838132-92c53300491e?auto=format&fit=crop&w=600&q=80',
|
||||
@@ -103,41 +91,6 @@ export default function App() {
|
||||
return STORE_COVERS[idx];
|
||||
};
|
||||
|
||||
// Live users mapped to display rows (rendered directly from the query).
|
||||
const users = (usersQ.data ?? []).map((u, i) => {
|
||||
const shift = fstr(u.shiftname).trim();
|
||||
return {
|
||||
userid: Number(u.userid),
|
||||
name:
|
||||
fstr(u.fullname).trim() ||
|
||||
`${fstr(u.firstname)} ${fstr(u.lastname)}`.trim() ||
|
||||
fstr(u.authname) ||
|
||||
'User',
|
||||
email: fstr(u.email) || fstr(u.authname) || '—',
|
||||
contact: fstr(u.contactno) || '—',
|
||||
roleid: Number(u.roleid),
|
||||
role: roleName(Number(u.roleid)),
|
||||
shift: shift && shift !== '-' ? shift : '—',
|
||||
location: fstr(u.applocation) || fstr(u.city) || 'Coimbatore',
|
||||
status: fstr(u.status) || 'Active',
|
||||
avatar: USER_AVATARS[i % USER_AVATARS.length],
|
||||
};
|
||||
});
|
||||
|
||||
// Role filter for the Users section ('ALL' or a numeric roleid).
|
||||
const [userRoleFilter, setUserRoleFilter] = useState<number | 'ALL'>('ALL');
|
||||
const filteredUsers = users.filter((u) => {
|
||||
const q = searchQuery.toLowerCase();
|
||||
const matchesSearch =
|
||||
!q ||
|
||||
u.name.toLowerCase().includes(q) ||
|
||||
u.email.toLowerCase().includes(q) ||
|
||||
u.contact.toLowerCase().includes(q);
|
||||
const matchesRole = userRoleFilter === 'ALL' || u.roleid === userRoleFilter;
|
||||
return matchesSearch && matchesRole;
|
||||
});
|
||||
const roleOptions = Array.from(new Set(users.map((u) => u.roleid)));
|
||||
|
||||
// Dynamic Secondary Modules list states (seeded from live data once it loads).
|
||||
const [storesList, setStoresList] = useState<Array<{ locationid?: number; name: string; zone: string; deliveries: number; sales: string; orders: number; staff: string; color: string; status: string }>>([]);
|
||||
|
||||
@@ -193,18 +146,9 @@ export default function App() {
|
||||
|
||||
// Secondary sub-sections modals triggers
|
||||
const [showAddStoreModal, setShowAddStoreModal] = useState(false);
|
||||
const [showAddUserModal, setShowAddUserModal] = useState(false);
|
||||
|
||||
// New forms states
|
||||
const [newStore, setNewStore] = useState({ name: '', zone: '', lead: '', sales: '₹1,50,000' });
|
||||
const [newUser, setNewUser] = useState({
|
||||
firstname: '',
|
||||
lastname: '',
|
||||
email: '',
|
||||
contactno: '',
|
||||
password: '',
|
||||
roleid: 4,
|
||||
});
|
||||
|
||||
// Form submission handles for secondary sections
|
||||
const handleCreateStore = (e: React.FormEvent) => {
|
||||
@@ -229,30 +173,6 @@ export default function App() {
|
||||
alert(`Node outlet "${newStore.name}" commissioned to live operations feed successfully.`);
|
||||
};
|
||||
|
||||
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.');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await createUserMut.mutateAsync({
|
||||
firstname: newUser.firstname,
|
||||
lastname: newUser.lastname,
|
||||
email: newUser.email,
|
||||
contactno: newUser.contactno,
|
||||
password: newUser.password,
|
||||
roleid: Number(newUser.roleid),
|
||||
tenantid: FIESTA_TENANT_ID,
|
||||
});
|
||||
setShowAddUserModal(false);
|
||||
setNewUser({ firstname: '', lastname: '', email: '', contactno: '', password: '', roleid: 4 });
|
||||
alert(`User "${newUser.firstname}" created successfully and synced to the live Users directory.`);
|
||||
} catch (err) {
|
||||
alert(`Could not create user: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Calendar Event Modal state
|
||||
const [showCalendarModal, setShowCalendarModal] = useState(false);
|
||||
|
||||
@@ -550,141 +470,6 @@ export default function App() {
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'users':
|
||||
return (
|
||||
<div className="space-y-lg animate-in fade-in duration-300">
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-md border-b border-[#e2e8f0] pb-xl">
|
||||
<div>
|
||||
<h1 className="font-sans font-bold text-2xl tracking-tight text-[#0f172a]">
|
||||
Users & Access
|
||||
</h1>
|
||||
<p className="text-zinc-500 font-sans text-xs mt-1">
|
||||
Tenant staff accounts, roles, assigned shifts, and account status — live from the Users API.
|
||||
</p>
|
||||
<div className="mt-1.5">
|
||||
{usersQ.isLoading ? (
|
||||
<span className="inline-flex items-center gap-1 text-[10px] font-semibold text-zinc-400 uppercase tracking-wide">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-zinc-300 animate-pulse" /> Loading live users…
|
||||
</span>
|
||||
) : usersQ.isError ? (
|
||||
<span className="inline-flex items-center gap-1 text-[10px] font-semibold text-rose-600 uppercase tracking-wide">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-rose-500" /> Live data unavailable
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1 text-[10px] font-semibold text-emerald-600 uppercase tracking-wide">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500" /> Live · {users.length} users
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => setShowAddUserModal(true)}
|
||||
className="bg-[#581c87] text-white px-xl py-2.5 rounded-lg text-xs font-bold uppercase tracking-wider flex items-center justify-center gap-xs cursor-pointer hover:bg-purple-800 transition"
|
||||
>
|
||||
Add User
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Role filter pills */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
onClick={() => setUserRoleFilter('ALL')}
|
||||
className={`px-3 py-1.5 rounded-lg text-xs font-semibold border transition-all cursor-pointer ${
|
||||
userRoleFilter === 'ALL'
|
||||
? 'bg-[#581c87] text-white border-[#581c87] shadow-sm'
|
||||
: 'bg-white text-zinc-700 border-[#e2e8f0] hover:bg-zinc-50'
|
||||
}`}
|
||||
>
|
||||
All Roles
|
||||
</button>
|
||||
{roleOptions.map((rid) => (
|
||||
<button
|
||||
key={rid}
|
||||
onClick={() => setUserRoleFilter(rid)}
|
||||
className={`px-3 py-1.5 rounded-lg text-xs font-semibold border transition-all cursor-pointer ${
|
||||
userRoleFilter === rid
|
||||
? 'bg-[#581c87] text-white border-[#581c87] shadow-sm'
|
||||
: 'bg-white text-zinc-700 border-[#e2e8f0] hover:bg-zinc-50'
|
||||
}`}
|
||||
>
|
||||
{roleName(rid)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Users table */}
|
||||
<div className="bg-white border border-[#e2e8f0] rounded-xl overflow-hidden shadow-sm">
|
||||
<div className="p-md border-b border-[#e2e8f0] bg-[#f8fafc] flex justify-between items-center">
|
||||
<h4 className="font-sans font-bold text-sm text-[#0f172a]">
|
||||
Directory ({filteredUsers.length})
|
||||
</h4>
|
||||
<span className="text-[10px] text-zinc-400 font-medium uppercase tracking-wider">Tenant {FIESTA_TENANT_ID}</span>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto text-xs font-sans">
|
||||
<table className="w-full text-left">
|
||||
<thead className="bg-[#f8fafc] border-b border-[#e2e8f0] text-zinc-500 text-[10px] uppercase font-bold tracking-wider">
|
||||
<tr>
|
||||
<th className="px-md py-sm">User</th>
|
||||
<th className="px-md py-sm">Role</th>
|
||||
<th className="px-md py-sm">Contact</th>
|
||||
<th className="px-md py-sm">Shift</th>
|
||||
<th className="px-md py-sm">Location</th>
|
||||
<th className="px-md py-sm text-right">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-[#f1f5f9]">
|
||||
{filteredUsers.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={6} className="text-center py-10 text-zinc-400">
|
||||
{usersQ.isLoading ? 'Loading live users…' : 'No users match this filter.'}
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
filteredUsers.map((u) => (
|
||||
<tr key={u.userid} className="hover:bg-[#f2f4f6]/50 transition-colors">
|
||||
<td className="px-md py-md">
|
||||
<div className="flex items-center gap-sm">
|
||||
<img
|
||||
src={u.avatar}
|
||||
alt={u.name}
|
||||
referrerPolicy="no-referrer"
|
||||
className="w-9 h-9 object-cover rounded-full border border-zinc-200 shrink-0"
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<p className="font-bold text-[#0f172a] truncate">{u.name}</p>
|
||||
<p className="text-[10px] text-zinc-400 font-medium truncate">{u.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-md py-md">
|
||||
<span className="px-2 py-0.5 rounded text-[9px] font-bold uppercase bg-purple-50 text-[#581c87] border border-purple-100">
|
||||
{u.role}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-md py-md font-mono text-zinc-600 font-medium">{u.contact}</td>
|
||||
<td className="px-md py-md text-zinc-500 font-medium">{u.shift}</td>
|
||||
<td className="px-md py-md text-zinc-500 font-medium">{u.location}</td>
|
||||
<td className="px-md py-md text-right">
|
||||
<span className={`px-1.5 py-0.5 rounded text-[9px] font-bold uppercase ${
|
||||
u.status.toLowerCase() === 'active'
|
||||
? 'text-emerald-700 bg-emerald-100'
|
||||
: 'text-zinc-500 bg-zinc-200'
|
||||
}`}>
|
||||
{u.status}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'settings':
|
||||
return <SettingsView tenantId={FIESTA_TENANT_ID} />;
|
||||
|
||||
@@ -742,8 +527,8 @@ export default function App() {
|
||||
<ReportsView searchQuery={searchQuery} isCoimbatoreView={isCoimbatoreView} />
|
||||
)}
|
||||
|
||||
{/* Handle alternative sections: Stores, Logistics, Staffing, Settings */}
|
||||
{['stores', 'users', 'settings'].includes(currentSection) &&
|
||||
{/* Handle alternative sections: Stores, Settings */}
|
||||
{['stores', 'settings'].includes(currentSection) &&
|
||||
renderSecondarySection()
|
||||
}
|
||||
</div>
|
||||
@@ -900,130 +685,6 @@ export default function App() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* CREATE NEW USER MODAL */}
|
||||
{showAddUserModal && (
|
||||
<div
|
||||
className="fixed inset-0 bg-[#0f172a]/40 backdrop-blur-sm z-[200] flex items-center justify-center p-md"
|
||||
onClick={(e) => { if (e.target === e.currentTarget) setShowAddUserModal(false); }}
|
||||
>
|
||||
<div className="bg-white border border-[#e2e8f0] rounded-xl w-full max-w-[26rem] max-h-[90vh] flex flex-col shadow-2xl overflow-hidden animate-in zoom-in-95 duration-200 text-xs font-sans cursor-default">
|
||||
<div className="p-md border-b border-[#e2e8f0] bg-[#f8fafc] flex justify-between items-center shrink-0">
|
||||
<h4 className="font-bold text-[#0f172a] flex items-center gap-xs">
|
||||
<Users size={15} className="text-[#581c87]" />
|
||||
Create User Account
|
||||
</h4>
|
||||
<button
|
||||
onClick={() => setShowAddUserModal(false)}
|
||||
className="p-1 hover:bg-zinc-200 rounded-full text-zinc-400 cursor-pointer transition-colors"
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleCreateUser} className="flex-1 flex flex-col min-h-0 overflow-hidden">
|
||||
<div className="p-md space-y-md overflow-y-auto flex-1">
|
||||
<p className="text-zinc-500 leading-relaxed">
|
||||
Creates a real user against the live Users API for tenant {FIESTA_TENANT_ID}.
|
||||
</p>
|
||||
<div className="space-y-sm">
|
||||
<div className="grid grid-cols-2 gap-sm">
|
||||
<div className="space-y-1">
|
||||
<label className="font-bold text-zinc-500 uppercase tracking-widest text-[9px]">FIRST NAME (*)</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="e.g. Harini"
|
||||
value={newUser.firstname}
|
||||
onChange={(e) => setNewUser({ ...newUser, firstname: e.target.value })}
|
||||
className="w-full border border-[#e2e8f0] rounded-lg p-sm bg-[#f8fafc] focus:bg-white outline-none focus:ring-1 focus:ring-[#581c87]"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label className="font-bold text-zinc-500 uppercase tracking-widest text-[9px]">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-[#e2e8f0] rounded-lg p-sm bg-[#f8fafc] focus:bg-white outline-none focus:ring-1 focus:ring-[#581c87]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="font-bold text-zinc-500 uppercase tracking-widest text-[9px]">EMAIL (*)</label>
|
||||
<input
|
||||
type="email"
|
||||
placeholder="e.g. harini@store.com"
|
||||
value={newUser.email}
|
||||
onChange={(e) => setNewUser({ ...newUser, email: e.target.value })}
|
||||
className="w-full border border-[#e2e8f0] rounded-lg p-sm bg-[#f8fafc] focus:bg-white outline-none focus:ring-1 focus:ring-[#581c87]"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-sm">
|
||||
<div className="space-y-1">
|
||||
<label className="font-bold text-zinc-500 uppercase tracking-widest text-[9px]">CONTACT NO (*)</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="9988776655"
|
||||
value={newUser.contactno}
|
||||
onChange={(e) => setNewUser({ ...newUser, contactno: e.target.value })}
|
||||
className="w-full border border-[#e2e8f0] rounded-lg p-sm bg-[#f8fafc] focus:bg-white outline-none focus:ring-1 focus:ring-[#581c87]"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label className="font-bold text-zinc-500 uppercase tracking-widest text-[9px]">ROLE</label>
|
||||
<select
|
||||
value={newUser.roleid}
|
||||
onChange={(e) => setNewUser({ ...newUser, roleid: Number(e.target.value) })}
|
||||
className="w-full border border-[#e2e8f0] rounded-lg p-2 bg-[#f8fafc] focus:bg-white outline-none"
|
||||
>
|
||||
<option value={1}>Owner</option>
|
||||
<option value={2}>Manager</option>
|
||||
<option value={3}>Admin</option>
|
||||
<option value={4}>Staff</option>
|
||||
<option value={6}>Cashier</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="font-bold text-zinc-500 uppercase tracking-widest text-[9px]">TEMPORARY PASSWORD (*)</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Set an initial password"
|
||||
value={newUser.password}
|
||||
onChange={(e) => setNewUser({ ...newUser, password: e.target.value })}
|
||||
className="w-full border border-[#e2e8f0] rounded-lg p-sm bg-[#f8fafc] focus:bg-white outline-none focus:ring-1 focus:ring-[#581c87] font-mono"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-md border-t border-[#f1f5f9] flex justify-end gap-sm bg-[#f8fafc] shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAddUserModal(false)}
|
||||
className="px-4 py-2 border border-[#e2e8f0] rounded-lg font-semibold text-zinc-500 hover:bg-zinc-50 cursor-pointer"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={createUserMut.isPending}
|
||||
className="px-4 py-2 bg-[#581c87] text-white rounded-lg font-bold hover:bg-purple-800 cursor-pointer shadow-sm disabled:opacity-60 disabled:cursor-not-allowed"
|
||||
>
|
||||
{createUserMut.isPending ? 'Creating…' : 'Create User'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user