1030 lines
50 KiB
TypeScript
1030 lines
50 KiB
TypeScript
/**
|
|
* @license
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
Network,
|
|
Truck,
|
|
Users,
|
|
Sliders,
|
|
Calendar,
|
|
AlertTriangle,
|
|
FileCheck,
|
|
Building,
|
|
CheckCircle2,
|
|
Clock,
|
|
ShieldCheck,
|
|
Send,
|
|
HelpCircle,
|
|
Database,
|
|
ArrowRight,
|
|
X,
|
|
Search,
|
|
Plus,
|
|
MapPin,
|
|
Phone,
|
|
Activity,
|
|
TrendingUp,
|
|
Award
|
|
} from 'lucide-react';
|
|
|
|
import { MainSection } from './types';
|
|
import {
|
|
useFiestaTenantLocations,
|
|
useFiestaLocationSummary,
|
|
useFiestaUsers,
|
|
useFiestaCreateUser,
|
|
} from './services/fiestaQueries';
|
|
import { FIESTA_TENANT_ID, str as fstr, roleName } from './services/fiestaApi';
|
|
import Sidebar from './components/Sidebar';
|
|
import Header from './components/Header';
|
|
import DashboardView from './components/DashboardView';
|
|
import OperationsView from './components/OperationsView';
|
|
import ReportsView from './components/ReportsView';
|
|
import InventoryView from './components/InventoryView';
|
|
import SettingsView from './components/SettingsView';
|
|
import StoreDetailView from './components/StoreDetailView';
|
|
import ragulStoreCover from './assets/images/store_front_view_1780299351800.png';
|
|
|
|
export default function App() {
|
|
// Navigation indicators states
|
|
const [currentSection, setCurrentSection] = useState<MainSection>('dashboard');
|
|
const [selectedStore, setSelectedStore] = useState<{ locationid?: number; name: string; zone: string; deliveries: number; sales: string; orders?: number; staff: string; color: string; status: string } | null>(null);
|
|
|
|
const handleSetSection = (sec: MainSection) => {
|
|
setCurrentSection(sec);
|
|
setSelectedStore(null);
|
|
};
|
|
|
|
const [isCoimbatoreView, setIsCoimbatoreView] = useState(false);
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
const [sidebarOpen, setSidebarOpen] = useState(true);
|
|
|
|
// ── 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.
|
|
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',
|
|
'https://images.unsplash.com/photo-1578916171728-46686eac8d58?auto=format&fit=crop&w=600&q=80',
|
|
'https://images.unsplash.com/photo-1604719312566-8912e9227c6a?auto=format&fit=crop&w=600&q=80',
|
|
'https://images.unsplash.com/photo-1534723452862-4c874018d66d?auto=format&fit=crop&w=600&q=80',
|
|
'https://images.unsplash.com/photo-1582408929130-98a2c2640b8a?auto=format&fit=crop&w=600&q=80',
|
|
'https://images.unsplash.com/photo-1516594798947-e65505dbb29d?auto=format&fit=crop&w=600&q=80',
|
|
'https://images.unsplash.com/photo-1601599561263-60a4e4e083cd?auto=format&fit=crop&w=600&q=80',
|
|
'https://images.unsplash.com/photo-1441986300917-64674bd600d8?auto=format&fit=crop&w=600&q=80',
|
|
'https://images.unsplash.com/photo-1528698827591-e19ccd7bc23d?auto=format&fit=crop&w=600&q=80',
|
|
'https://images.unsplash.com/photo-1536697246787-1f7ae568d89a?auto=format&fit=crop&w=600&q=80',
|
|
'https://images.unsplash.com/photo-1506617498306-bd97b3663b65?auto=format&fit=crop&w=600&q=80',
|
|
'https://images.unsplash.com/photo-1579621970563-ebec7560ff3e?auto=format&fit=crop&w=600&q=80',
|
|
];
|
|
|
|
const getStoreCover = (name: string) => {
|
|
if (name.toLowerCase().includes('ragul')) return ragulStoreCover;
|
|
let hash = 0;
|
|
for (let j = 0; j < name.length; j++) {
|
|
hash = name.charCodeAt(j) + ((hash << 5) - hash);
|
|
}
|
|
const idx = Math.abs(hash) % STORE_COVERS.length;
|
|
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 }>>([]);
|
|
|
|
const [storesSearch, setStoresSearch] = useState('');
|
|
const [storesFilter, setStoresFilter] = useState<'ALL' | 'ACTIVE' | 'CRITICAL'>('ALL');
|
|
|
|
const filteredStoresList = storesList.filter((st) => {
|
|
const q = storesSearch.toLowerCase();
|
|
const matchesSearch =
|
|
!q ||
|
|
st.name.toLowerCase().includes(q) ||
|
|
st.zone.toLowerCase().includes(q) ||
|
|
st.staff.toLowerCase().includes(q);
|
|
|
|
if (storesFilter === 'ACTIVE') {
|
|
return matchesSearch && st.status.toLowerCase() === 'active';
|
|
}
|
|
if (storesFilter === 'CRITICAL') {
|
|
return (
|
|
matchesSearch &&
|
|
(st.status.toLowerCase() !== 'active' || st.deliveries > 40)
|
|
);
|
|
}
|
|
return matchesSearch;
|
|
});
|
|
|
|
const activeCount = storesList.filter((st) => st.status.toLowerCase() === 'active').length;
|
|
const totalCount = storesList.length;
|
|
const totalDeliveries = storesList.reduce((acc, st) => acc + st.deliveries, 0);
|
|
|
|
useEffect(() => {
|
|
const locations = locationsQ.data ?? [];
|
|
const summaries = locSummaryQ.data ?? [];
|
|
if (locations.length) {
|
|
setStoresList(
|
|
locations.map((loc) => {
|
|
const sum = summaries.find((s) => s.locationid === Number(loc.locationid));
|
|
return {
|
|
locationid: Number(loc.locationid),
|
|
name: fstr(loc.locationname) || `Location ${fstr(loc.locationid)}`,
|
|
zone: [fstr(loc.suburb), fstr(loc.city)].filter(Boolean).join(', ') || 'Coimbatore',
|
|
deliveries: sum?.delivered ?? 0,
|
|
sales: `${(sum?.total ?? 0).toLocaleString('en-IN')} orders`,
|
|
orders: Math.max(sum?.delivered ?? 0, sum?.total ?? 0),
|
|
staff: fstr(loc.contactno) || fstr(loc.email) || '—',
|
|
color: fstr(loc.status).toLowerCase() === 'active' ? 'emerald' : 'amber',
|
|
status: fstr(loc.status) || 'Active',
|
|
};
|
|
}),
|
|
);
|
|
}
|
|
}, [locationsQ.data, locSummaryQ.data]);
|
|
|
|
// 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) => {
|
|
e.preventDefault();
|
|
if (!newStore.name || !newStore.zone || !newStore.lead) {
|
|
alert('Kindly fill store metadata completely.');
|
|
return;
|
|
}
|
|
setStoresList([...storesList, {
|
|
locationid: 10000 + Math.floor(Math.random() * 9000),
|
|
name: newStore.name,
|
|
zone: newStore.zone,
|
|
deliveries: 0,
|
|
sales: '0 orders',
|
|
orders: 0,
|
|
staff: newStore.lead,
|
|
color: 'emerald',
|
|
status: 'Active'
|
|
}]);
|
|
setShowAddStoreModal(false);
|
|
setNewStore({ name: '', zone: '', lead: '', sales: '₹1,50,000' });
|
|
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);
|
|
|
|
// Callback action triggers
|
|
const handleNewReport = () => {
|
|
setCurrentSection('reports');
|
|
alert('System routed back to reports dashboard interface. Select product item metadata matrices.');
|
|
};
|
|
|
|
const handleHelp = () => {
|
|
alert('nearledaily User Manual & Documentation Center linked successfully. Contact Coimbatore regional IT hub desk for urgent escalations.');
|
|
};
|
|
|
|
const handleLogout = () => {
|
|
const ok = window.confirm('Are you sure you want to terminate this active secure session?');
|
|
if (ok) {
|
|
alert('Secure session suspended. Page reloading and restarting database state simulation.');
|
|
window.location.reload();
|
|
}
|
|
};
|
|
|
|
// Define secondary sections (Stores, Logistics, Staffing, Settings) within main body
|
|
const renderSecondarySection = () => {
|
|
switch (currentSection) {
|
|
case 'stores':
|
|
if (selectedStore) {
|
|
return (
|
|
<StoreDetailView
|
|
store={selectedStore}
|
|
onBack={() => setSelectedStore(null)}
|
|
/>
|
|
);
|
|
}
|
|
return (
|
|
<div className="space-y-lg animate-in fade-in duration-300">
|
|
{/* Simple and elegant premium header */}
|
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 pb-6 border-b border-zinc-205 mb-6">
|
|
<div>
|
|
<h1 className="font-sans font-bold text-2xl tracking-tight text-[#0f172a]">
|
|
Stores Registry
|
|
</h1>
|
|
<p className="text-zinc-500 font-sans text-xs mt-1">
|
|
Local nodes registry, active manager assignments, and live dispatch and grocery delivery fulfillment statistics.
|
|
</p>
|
|
</div>
|
|
|
|
<button
|
|
onClick={() => setShowAddStoreModal(true)}
|
|
className="bg-[#581c87] text-white px-5 py-2.5 rounded-lg text-xs font-bold uppercase tracking-wider flex items-center justify-center gap-2 cursor-pointer hover:bg-purple-800 transition shadow-sm"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
Add Retail Outlet Node
|
|
</button>
|
|
</div>
|
|
|
|
{/* Filter control bar */}
|
|
<div className="flex flex-col md:flex-row items-center justify-between gap-4 bg-white p-4 rounded-xl border border-[#e2e8f0] shadow-sm mb-6">
|
|
{/* Search input with search icon */}
|
|
<div className="relative w-full md:max-w-md">
|
|
<span className="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none text-zinc-400">
|
|
<Search className="w-4 h-4" />
|
|
</span>
|
|
<input
|
|
type="text"
|
|
placeholder="Search stores by name, zone, or manager..."
|
|
value={storesSearch}
|
|
onChange={(e) => setStoresSearch(e.target.value)}
|
|
className="w-full pl-10 pr-4 py-2 bg-zinc-50 border border-zinc-200 rounded-lg text-xs font-medium text-zinc-800 placeholder-zinc-400 focus:outline-none focus:ring-2 focus:ring-[#581c87]/20 focus:border-[#581c87] transition-all"
|
|
/>
|
|
{storesSearch && (
|
|
<button
|
|
onClick={() => setStoresSearch('')}
|
|
className="absolute inset-y-0 right-0 pr-3 flex items-center text-zinc-400 hover:text-zinc-600"
|
|
>
|
|
<X className="w-3.5 h-3.5" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{/* Filter tabs */}
|
|
<div className="flex items-center bg-zinc-100/80 p-1 rounded-lg border border-zinc-200/50 w-full md:w-auto">
|
|
<button
|
|
onClick={() => setStoresFilter('ALL')}
|
|
className={`flex-1 md:flex-none px-4 py-1.5 rounded-md text-xs font-bold transition-all ${
|
|
storesFilter === 'ALL'
|
|
? 'bg-white text-[#581c87] shadow-sm'
|
|
: 'text-zinc-500 hover:text-zinc-800 hover:bg-white/40'
|
|
}`}
|
|
>
|
|
All Nodes ({storesList.length})
|
|
</button>
|
|
<button
|
|
onClick={() => setStoresFilter('ACTIVE')}
|
|
className={`flex-1 md:flex-none px-4 py-1.5 rounded-md text-xs font-bold transition-all ${
|
|
storesFilter === 'ACTIVE'
|
|
? 'bg-white text-emerald-600 shadow-sm'
|
|
: 'text-zinc-500 hover:text-emerald-600 hover:bg-white/40'
|
|
}`}
|
|
>
|
|
Active ({storesList.filter(s => s.status.toLowerCase() === 'active').length})
|
|
</button>
|
|
<button
|
|
onClick={() => setStoresFilter('CRITICAL')}
|
|
className={`flex-1 md:flex-none px-4 py-1.5 rounded-md text-xs font-bold transition-all ${
|
|
storesFilter === 'CRITICAL'
|
|
? 'bg-white text-rose-600 shadow-sm'
|
|
: 'text-zinc-500 hover:text-rose-650 hover:bg-white/40'
|
|
}`}
|
|
>
|
|
Critical Alerts ({storesList.filter(s => s.status.toLowerCase() !== 'active' || s.deliveries > 40 || s.deliveries === 0).length})
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Empty States */}
|
|
{filteredStoresList.length === 0 && (
|
|
<div className="text-center py-12 text-zinc-400 text-xs border border-dashed border-[#e2e8f0] rounded-xl bg-white p-8">
|
|
{locationsQ.isLoading ? (
|
|
<div className="flex flex-col items-center justify-center gap-2">
|
|
<div className="w-6 h-6 border-2 border-[#581c87] border-t-transparent rounded-full animate-spin" />
|
|
<span>Loading live store locations…</span>
|
|
</div>
|
|
) : (
|
|
<span>No store locations found matching your filter criteria.</span>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Immersive Background Blur Blobs */}
|
|
<div className="relative">
|
|
<div className="absolute top-10 left-10 w-72 h-72 bg-purple-400/10 rounded-full blur-[100px] pointer-events-none -z-10 animate-pulse" />
|
|
<div className="absolute bottom-10 right-10 w-80 h-80 bg-indigo-400/10 rounded-full blur-[100px] pointer-events-none -z-10 animate-pulse" style={{ animationDuration: '6s' }} />
|
|
|
|
{/* Store Cards Grid */}
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-gutter relative z-10">
|
|
{filteredStoresList.map((st, i) => {
|
|
const totalOrders = st.orders ?? st.deliveries ?? 0;
|
|
const fulfillmentRate = totalOrders > 0 ? Math.min(100, Math.round((st.deliveries / totalOrders) * 100)) : 100;
|
|
|
|
return (
|
|
<div
|
|
key={st.locationid || i}
|
|
onClick={() => setSelectedStore(st)}
|
|
className={`group relative overflow-hidden bg-white/70 backdrop-blur-md border border-zinc-200/80 rounded-2xl shadow-sm hover:shadow-[0_20px_40px_rgba(88,28,135,0.12)] transition-all duration-500 cursor-pointer flex flex-col ${
|
|
st.status.toLowerCase() === 'active'
|
|
? st.deliveries > 40
|
|
? 'hover:border-rose-300'
|
|
: 'hover:border-emerald-300'
|
|
: 'hover:border-amber-300'
|
|
}`}
|
|
>
|
|
{/* Card Cover Image with Zoom effect */}
|
|
<div className="relative h-32 w-full overflow-hidden flex-shrink-0">
|
|
<img
|
|
src={getStoreCover(st.name)}
|
|
alt={st.name}
|
|
className="w-full h-full object-cover group-hover:scale-108 transition-transform duration-700 ease-out"
|
|
/>
|
|
<div className="absolute inset-0 bg-gradient-to-t from-slate-900/90 via-slate-900/20 to-transparent" />
|
|
|
|
{/* Status Badge */}
|
|
<div className="absolute top-3 right-3">
|
|
<span className={`px-2.5 py-0.5 rounded-full text-[9px] font-bold uppercase tracking-wider flex items-center gap-1 border backdrop-blur-md ${
|
|
st.status.toLowerCase() === 'active'
|
|
? st.deliveries > 40
|
|
? 'text-rose-200 bg-rose-950/60 border-rose-500/30'
|
|
: 'text-emerald-200 bg-emerald-950/60 border-emerald-500/30'
|
|
: 'text-amber-200 bg-amber-950/60 border-amber-500/30'
|
|
}`}>
|
|
<span className="w-1.5 h-1.5 rounded-full bg-current animate-pulse" />
|
|
{st.status.toLowerCase() === 'active' && st.deliveries > 40 ? 'High Load' : st.status}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Zone & Title */}
|
|
<div className="absolute bottom-3 left-3 right-3 text-white">
|
|
<p className="text-[9px] text-purple-200 font-bold uppercase tracking-widest leading-none mb-1">{st.zone}</p>
|
|
<h3 className="font-sans font-bold text-sm leading-tight text-white group-hover:text-purple-200 transition-colors">{st.name}</h3>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Card Content Area */}
|
|
<div className="p-5 flex-1 flex flex-col justify-between">
|
|
{/* Metrics Row & Progress Circle */}
|
|
<div className="flex items-center justify-between gap-4">
|
|
<div className="grid grid-cols-2 gap-4 flex-1">
|
|
<div>
|
|
<span className="text-[9px] text-zinc-400 uppercase tracking-widest font-bold block">Deliveries</span>
|
|
<p className="font-extrabold text-base text-slate-900 mt-0.5 font-mono">{st.deliveries.toLocaleString()}</p>
|
|
<span className="text-[8px] text-emerald-600 font-semibold block mt-0.5">Dispatched Today</span>
|
|
</div>
|
|
<div>
|
|
<span className="text-[9px] text-zinc-400 uppercase tracking-widest font-bold block">Total Orders</span>
|
|
<p className="font-extrabold text-base text-[#581c87] mt-0.5 font-mono">{totalOrders.toLocaleString()}</p>
|
|
<span className="text-[8px] text-purple-600 font-semibold block mt-0.5">Incoming Volume</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Circular Progress Ring */}
|
|
<div className="relative flex items-center justify-center flex-shrink-0" title={`Fulfillment Rate: ${fulfillmentRate}%`}>
|
|
<svg className="w-12 h-12 transform -rotate-90">
|
|
<circle
|
|
cx="24"
|
|
cy="24"
|
|
r="18"
|
|
className="stroke-zinc-100"
|
|
strokeWidth="3.5"
|
|
fill="transparent"
|
|
/>
|
|
<circle
|
|
cx="24"
|
|
cy="24"
|
|
r="18"
|
|
className={`transition-all duration-500 ${
|
|
st.status.toLowerCase() === 'active'
|
|
? st.deliveries > 40
|
|
? 'stroke-rose-500'
|
|
: 'stroke-emerald-500'
|
|
: 'stroke-amber-500'
|
|
}`}
|
|
strokeWidth="3.5"
|
|
fill="transparent"
|
|
strokeDasharray="113"
|
|
strokeDashoffset={113 - (113 * fulfillmentRate) / 100}
|
|
strokeLinecap="round"
|
|
/>
|
|
</svg>
|
|
<span className="absolute text-[9px] font-extrabold text-zinc-700 font-mono">
|
|
{fulfillmentRate}%
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Live Sparkline Trend Histogram */}
|
|
<div className="mt-4 pt-3.5 border-t border-zinc-100">
|
|
<div className="flex justify-between items-center text-[8px] text-zinc-400 font-bold uppercase tracking-widest mb-1.5">
|
|
<span>Speed Index (Live Feed)</span>
|
|
<span className="text-[#581c87] flex items-center gap-0.5 text-[8px] font-bold">
|
|
<Activity className="w-2.5 h-2.5 animate-pulse" /> Live
|
|
</span>
|
|
</div>
|
|
<div className="h-6 flex items-end gap-1">
|
|
{[30, 48, 25, 62, 54, 75, 42, 80, Math.min(95, Math.max(15, st.deliveries * 1.8))].map((val, idx) => (
|
|
<div key={idx} className="flex-1 bg-zinc-100 rounded-t-sm h-full relative overflow-hidden group/bar">
|
|
<div
|
|
style={{ height: `${val}%` }}
|
|
className={`absolute bottom-0 left-0 right-0 rounded-t-sm transition-all duration-500 ${
|
|
st.status.toLowerCase() === 'active'
|
|
? st.deliveries > 40
|
|
? 'bg-rose-500/80 group-hover/bar:bg-rose-500'
|
|
: 'bg-[#581c87]/70 group-hover/bar:bg-[#581c87]'
|
|
: 'bg-amber-500/70 group-hover/bar:bg-amber-500'
|
|
}`}
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Lead Manager Profile block */}
|
|
<div className="flex justify-between items-center text-xs text-zinc-650 bg-zinc-50/80 rounded-xl p-2.5 border border-zinc-100/80 mt-4">
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-7 h-7 rounded-full bg-gradient-to-tr from-purple-600 to-indigo-600 text-white flex items-center justify-center font-bold text-[10px] border border-white shadow-sm flex-shrink-0">
|
|
{st.staff.slice(0, 2).toUpperCase()}
|
|
</div>
|
|
<div className="min-w-0">
|
|
<span className="text-[8px] text-zinc-400 block font-semibold leading-none mb-0.5">Node Lead</span>
|
|
<span className="font-bold text-[#0f172a] text-xs truncate max-w-[100px] block leading-none">{st.staff}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
alert(`Routing communications channel directly to manager ${st.staff}...`);
|
|
}}
|
|
className="p-1.5 rounded-lg bg-white border border-zinc-200 hover:border-[#581c87] hover:text-[#581c87] text-zinc-500 transition-colors shadow-sm"
|
|
title="Communicate with Node Lead"
|
|
>
|
|
<Phone className="w-3.5 h-3.5" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Card footer - enter console */}
|
|
<div className="flex items-center justify-between text-[10px] font-bold text-[#581c87] mt-4 pt-3 border-t border-zinc-100/80">
|
|
<span className="uppercase tracking-wider">Enter Terminal Console</span>
|
|
<ArrowRight className="w-3.5 h-3.5 transform group-hover:translate-x-1.5 transition-transform duration-350 text-[#581c87]" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</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} />;
|
|
|
|
default:
|
|
return null;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen bg-[#f8fafc] text-[#0f172a] font-sans antialiased">
|
|
{/* Navbar segment */}
|
|
<Header
|
|
currentSection={currentSection}
|
|
setCurrentSection={handleSetSection}
|
|
isCoimbatoreView={isCoimbatoreView}
|
|
onToggleSidebar={() => setSidebarOpen((prev) => !prev)}
|
|
isSidebarOpen={sidebarOpen}
|
|
onNewReportClick={handleNewReport}
|
|
onHelpClick={handleHelp}
|
|
onLogoutClick={handleLogout}
|
|
/>
|
|
|
|
{/* Main Container workspace layout splits */}
|
|
<div className="flex pt-20">
|
|
|
|
{/* Interactive Left Rail */}
|
|
<Sidebar
|
|
currentSection={currentSection}
|
|
setCurrentSection={handleSetSection}
|
|
isCoimbatoreView={isCoimbatoreView}
|
|
setIsCoimbatoreView={setIsCoimbatoreView}
|
|
isOpen={sidebarOpen}
|
|
/>
|
|
|
|
{/* Main core pages payload area */}
|
|
<main className={`flex-1 min-w-0 min-h-[calc(100vh-80px)] transition-all duration-300 ${sidebarOpen ? 'md:pl-64' : 'md:pl-20'}`}>
|
|
<div className="w-full p-container-margin md:p-xl space-y-lg transition-all duration-300">
|
|
{/* Nav content routing */}
|
|
{currentSection === 'dashboard' && (
|
|
<DashboardView searchQuery={searchQuery} isCoimbatoreView={isCoimbatoreView} />
|
|
)}
|
|
|
|
{currentSection === 'inventory' && (
|
|
<InventoryView
|
|
searchQuery={searchQuery}
|
|
isCoimbatoreView={isCoimbatoreView}
|
|
/>
|
|
)}
|
|
|
|
{currentSection === 'operations' && (
|
|
<OperationsView searchQuery={searchQuery} isCoimbatoreView={isCoimbatoreView} />
|
|
)}
|
|
|
|
{currentSection === 'reports' && (
|
|
<ReportsView searchQuery={searchQuery} isCoimbatoreView={isCoimbatoreView} />
|
|
)}
|
|
|
|
{/* Handle alternative sections: Stores, Logistics, Staffing, Settings */}
|
|
{['stores', 'users', 'settings'].includes(currentSection) &&
|
|
renderSecondarySection()
|
|
}
|
|
</div>
|
|
</main>
|
|
</div>
|
|
|
|
{/* CALENDAR SCHEDULER DIALOG MODAL */}
|
|
{showCalendarModal && (
|
|
<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) setShowCalendarModal(false); }}
|
|
>
|
|
<div className="bg-white border border-[#e2e8f0] rounded-xl w-full max-w-[24rem] 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">
|
|
<Calendar size={15} className="text-[#581c87]" />
|
|
Scheduled Reports Calendar
|
|
</h4>
|
|
<button
|
|
onClick={() => setShowCalendarModal(false)}
|
|
className="p-1 hover:bg-zinc-200 rounded-full text-zinc-400 cursor-pointer transition-colors"
|
|
>
|
|
<X size={16} />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="p-md space-y-md overflow-y-auto flex-1">
|
|
<p className="text-zinc-500 leading-relaxed font-semibold">
|
|
Automated compliance summaries are scheduled to generate and export on the following dates:
|
|
</p>
|
|
|
|
<div className="divide-y divide-[#f1f5f9] select-none text-[11px]">
|
|
<div className="py-2 flex justify-between items-center">
|
|
<span className="font-semibold text-zinc-700">Monthly Assortment Audit Ledger</span>
|
|
<span className="font-mono text-[#581c87] font-bold">Oct 31, 2023</span>
|
|
</div>
|
|
<div className="py-2 flex justify-between items-center">
|
|
<span className="font-semibold text-zinc-700">Daily Regional Turnover Sheet</span>
|
|
<span className="text-emerald-600 font-bold">Everyday 23:59 (GMT)</span>
|
|
</div>
|
|
<div className="py-2 flex justify-between items-center">
|
|
<span className="font-semibold text-zinc-700">Q4 Outlook Forecast Draft</span>
|
|
<span className="font-mono text-zinc-500 font-bold">Nov 15, 2023</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="p-sm bg-amber-50 border border-amber-100 rounded-lg flex gap-sm text-amber-900 font-medium">
|
|
<AlertTriangle size={16} className="shrink-0 mt-0.5" />
|
|
<span>Next automated sync will occur at standard local closing hour thresholds.</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="p-sm bg-[#f8fafc] border-t border-[#e2e8f0] flex justify-end shrink-0">
|
|
<button
|
|
onClick={() => setShowCalendarModal(false)}
|
|
className="px-4 py-2 bg-[#0f172a] text-white rounded-lg font-semibold hover:bg-zinc-800 cursor-pointer shadow-sm transition-colors"
|
|
>
|
|
Close Calendar
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* CREATE NEW STORE MODAL */}
|
|
{showAddStoreModal && (
|
|
<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) setShowAddStoreModal(false); }}
|
|
>
|
|
<div className="bg-white border border-[#e2e8f0] rounded-xl w-full max-w-[24rem] 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">
|
|
<Building size={15} className="text-[#581c87]" />
|
|
Commission New Regional Store Node
|
|
</h4>
|
|
<button
|
|
onClick={() => setShowAddStoreModal(false)}
|
|
className="p-1 hover:bg-zinc-200 rounded-full text-zinc-400 cursor-pointer transition-colors"
|
|
>
|
|
<X size={16} />
|
|
</button>
|
|
</div>
|
|
|
|
<form onSubmit={handleCreateStore} className="flex-1 flex flex-col min-h-0 overflow-hidden">
|
|
<div className="p-md space-y-md overflow-y-auto flex-1">
|
|
<div className="space-y-sm">
|
|
<div className="space-y-1">
|
|
<label className="font-bold text-zinc-500 uppercase tracking-widest text-[9px]">STORE OUTLET NAME (*)</label>
|
|
<input
|
|
type="text"
|
|
placeholder="e.g. RS Puram Super Hub"
|
|
value={newStore.name}
|
|
onChange={(e) => setNewStore({ ...newStore, name: 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]">LOCAL ZONE AREA (*)</label>
|
|
<input
|
|
type="text"
|
|
placeholder="e.g. Coimbatore North"
|
|
value={newStore.zone}
|
|
onChange={(e) => setNewStore({ ...newStore, zone: 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]">OUTLET TEAM MANAGER (*)</label>
|
|
<input
|
|
type="text"
|
|
placeholder="e.g. Sridhar Sundaram"
|
|
value={newStore.lead}
|
|
onChange={(e) => setNewStore({ ...newStore, lead: 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]">ESTIMATED INITIAL REVENUE</label>
|
|
<input
|
|
type="text"
|
|
placeholder="₹1,50,000"
|
|
value={newStore.sales}
|
|
onChange={(e) => setNewStore({ ...newStore, sales: 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>
|
|
|
|
<div className="p-md border-t border-[#f1f5f9] flex justify-end gap-sm bg-[#f8fafc] shrink-0">
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowAddStoreModal(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"
|
|
className="px-4 py-2 bg-[#581c87] text-white rounded-lg font-bold hover:bg-purple-800 cursor-pointer shadow-sm"
|
|
>
|
|
Create Outlet Node
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</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>
|
|
);
|
|
}
|