+
{(['All', 'Healthy', 'Low Stock', 'Critical'] as const).map((filter) => (
setStockFilter(filter)}
- className={`px-3 py-1 rounded-lg cursor-pointer transition-colors ${
- stockFilter === filter
- ? 'bg-[#581c87] text-white shadow-sm'
- : 'bg-white border border-[#e2e8f0] text-zinc-600 hover:bg-zinc-50'
- }`}
+ className={`px-3 py-1.5 rounded-lg cursor-pointer transition-colors border ${stockFilter === filter
+ ? 'bg-[#581c87] text-white border-[#581c87] shadow-sm'
+ : 'bg-white border-[#e2e8f0] text-zinc-655 hover:bg-zinc-55'
+ }`}
>
{filter}
))}
triggerExport('CSV')}
- className="px-3 py-1 bg-[#0f172a] text-white rounded-lg cursor-pointer hover:bg-zinc-800 transition-colors shadow-sm"
+ onClick={() => startExportSim('CSV')}
+ className="px-3 py-1.5 bg-[#0f172a] text-white rounded-lg cursor-pointer hover:bg-zinc-805 border border-[#0f172a] transition-colors shadow-sm"
>
Export CSV
@@ -369,8 +1120,9 @@ export default function ReportsView({ searchQuery, isCoimbatoreView }: ReportsVi
{/* Matrix Data table */}
-
+
+
Product Name
SKU ID
Units Sold
@@ -382,89 +1134,198 @@ export default function ReportsView({ searchQuery, isCoimbatoreView }: ReportsVi
{filteredProducts.length === 0 ? (
-
+
No matching items matching stock filter criteria.
) : (
- filteredProducts.map((prod) => (
-
-
-
-
-
- {prod.name}
-
-
- {prod.sku}
-
-
- {prod.unitsSold.toLocaleString()}
-
-
- ₹{prod.revenue.toLocaleString()}
-
-
-
- {prod.stockStatus}
-
-
-
-
- {prod.trend === 'up' ? (
-
- ) : prod.trend === 'down' ? (
-
- ) : (
-
- )}
-
-
-
- ))
+ filteredProducts.map((prod) => {
+ const isExpanded = expandedProductId === prod.id;
+ return (
+
+ setExpandedProductId(isExpanded ? null : prod.id)}
+ className={`hover:bg-[#f2f4f6]/40 transition-all font-medium text-zinc-700 cursor-pointer ${isExpanded ? 'bg-slate-50/50' : ''}`}
+ >
+
+
+
+
+
+
+
+
+ {prod.name}
+ {prod.category}
+
+
+
+ {prod.sku}
+
+
+ {prod.unitsSold.toLocaleString()}
+
+
+ ₹{prod.revenue.toLocaleString()}
+
+
+
+ {prod.stockStatus}
+
+
+
+
+ {prod.trend === 'up' ? (
+
+ ) : prod.trend === 'down' ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {/* Expanded details row */}
+ {isExpanded && (
+
+
+
+
+
+ {/* Inventory Level Progress block */}
+
+
+ Stock Capacity Index
+
+
+
+ Current Balance
+
+ {prod.stockStatus === 'Healthy' ? '142 Units (Optimal)' :
+ prod.stockStatus === 'Low Stock' ? '42 Units (Low)' : '6 Units (Critical)'}
+
+
+
+
+
+
+ {/* Distribution Locations */}
+
+
+ Hub Distribution Allocations
+
+
+
+ Saravanampatti Hub
+ {prod.stockStatus === 'Healthy' ? '85 units' : prod.stockStatus === 'Low Stock' ? '25 units' : '4 units'}
+
+
+
+ RS Puram Hub
+ {prod.stockStatus === 'Healthy' ? '57 units' : prod.stockStatus === 'Low Stock' ? '17 units' : '2 units'}
+
+
+
+
+
+ {/* System Audit */}
+
+
+ Metadata & Barcode Identification
+
+
+
+
+ Warehouse Bin
+ BIN-C{prod.sku.replace(/\D/g, '').slice(-3) || '042'}
+
+
+ Last Audited
+ {new Date().toLocaleDateString('en-IN', { day: 'numeric', month: 'short', year: 'numeric' })}
+
+
+
+ {/* Monospace barcode simulation */}
+
+
+ {[1, 3, 1, 2, 4, 1, 3, 2, 1, 2, 3, 1, 2, 4, 1, 2].map((w, idx) => (
+
+ ))}
+
+
{prod.sku}
+
+
+
+
+
+
+
+
+ )}
+
+ );
+ })
)}
{/* Matrix table pagination */}
-
+
Showing 1-{filteredProducts.length} of {liveProducts.length} live products
-
+
- setCurrentPage(prev => Math.max(1, prev - 1))}
className="w-8 h-8 border border-[#e2e8f0] rounded-lg flex items-center justify-center hover:bg-white active:bg-[#f8fafc] cursor-pointer"
>
-
+
1
- alert('Proceeding to page 2 details representation')}
- className="w-8 h-8 border border-[#e2e8f0] rounded-lg flex items-center justify-center hover:bg-white text-zinc-500 font-bold font-mono text-[10px] cursor-pointer"
+ className="w-8 h-8 border border-[#e2e8f0] rounded-lg flex items-center justify-center hover:bg-white text-zinc-550 font-bold font-mono text-[10px] cursor-pointer"
>
2
- alert('Proceeding to page 3 details representation')}
- className="w-8 h-8 border border-[#e2e8f0] rounded-lg flex items-center justify-center hover:bg-white text-zinc-500 font-bold font-mono text-[10px] cursor-pointer"
+ className="w-8 h-8 border border-[#e2e8f0] rounded-lg flex items-center justify-center hover:bg-white text-zinc-555 font-bold font-mono text-[10px] cursor-pointer"
>
3
- setCurrentPage(prev => prev + 1)}
className="w-8 h-8 border border-[#e2e8f0] rounded-lg flex items-center justify-center hover:bg-white active:bg-[#f8fafc] cursor-pointer"
>
@@ -473,6 +1334,33 @@ export default function ReportsView({ searchQuery, isCoimbatoreView }: ReportsVi
+
+ {/* EXPORTING DIALOG MODAL */}
+ {exportingFormat && (
+
+
+
+
+
+
+ Generating {exportingFormat} Report
+
+
+ Compiling database records and SVG vector curves...
+
+
+ {/* Progress track */}
+
+
{exportProgress}%
+
+
+ )}
+
);
}
diff --git a/src/components/SettingsView.tsx b/src/components/SettingsView.tsx
index 82f74b1..01ec198 100644
--- a/src/components/SettingsView.tsx
+++ b/src/components/SettingsView.tsx
@@ -17,6 +17,7 @@ import {
Check,
RotateCcw,
CheckCircle2,
+ Plus
} from 'lucide-react';
import { useFiestaAllTenants, useFiestaTenantLocations } from '../services/fiestaQueries';
import { FIESTA_TENANT_ID, str as fstr, num as fnum, roleName } from '../services/fiestaApi';
@@ -87,12 +88,50 @@ function loadSettings(): { settings: MerchantSettings; hadSaved: boolean } {
return { settings: { ...DEFAULTS }, hadSaved: false };
}
-// ── Small presentational helpers ────────────────────────────────────────────
+// Localized fallback dataset to replace generic Faker test data with realistic Coimbatore outlets
+const LOCAL_OUTLETS_DATA = [
+ { name: 'Ragul Stores - Gandhipuram Hub', suburb: 'Gandhipuram', city: 'Coimbatore', postcode: '641018', radius: 4500, mins: 30 },
+ { name: 'Ragul Stores - Peelamedu Hub', suburb: 'Peelamedu', city: 'Coimbatore', postcode: '641004', radius: 3500, mins: 25 },
+ { name: 'Ragul Stores - RS Puram Hub', suburb: 'RS Puram', city: 'Coimbatore', postcode: '641002', radius: 5000, mins: 35 },
+ { name: 'Ragul Stores - Saravanampatti Outlet', suburb: 'Saravanampatti', city: 'Coimbatore', postcode: '641035', radius: 6000, mins: 40 },
+ { name: 'Ragul Stores - Singanallur Outlet', suburb: 'Singanallur', city: 'Coimbatore', postcode: '641005', radius: 4000, mins: 30 },
+ { name: 'Ragul Stores - Vadavalli Hub', suburb: 'Vadavalli', city: 'Coimbatore', postcode: '641046', radius: 3000, mins: 20 },
+ { name: 'Ragul Stores - Ramanathapuram Hub', suburb: 'Ramanathapuram', city: 'Coimbatore', postcode: '641045', radius: 4500, mins: 30 },
+ { name: 'Ragul Stores - Town Hall Outlet', suburb: 'Town Hall', city: 'Coimbatore', postcode: '641001', radius: 3500, mins: 25 },
+];
+
+const formatFriendlyTime = (timeStr: string) => {
+ try {
+ if (timeStr.includes('T')) {
+ const parts = timeStr.split('T')[1].split(':');
+ let hour = parseInt(parts[0], 10);
+ const min = parts[1];
+ const ampm = hour >= 12 ? 'PM' : 'AM';
+ hour = hour % 12;
+ hour = hour ? hour : 12;
+ return `${hour}:${min} ${ampm}`;
+ }
+ if (timeStr.includes(':')) {
+ const parts = timeStr.split(':');
+ let hour = parseInt(parts[0], 10);
+ const min = parts[1].slice(0, 2);
+ const ampm = hour >= 12 ? 'PM' : 'AM';
+ hour = hour % 12;
+ hour = hour ? hour : 12;
+ return `${hour}:${min} ${ampm}`;
+ }
+ } catch {
+ // fallback
+ }
+ return timeStr;
+};
+
+/// ── Small presentational helpers ────────────────────────────────────────────
function Toggle({ checked, onChange }: { checked: boolean; onChange: () => void }) {
return (
-
+
-
+
);
}
@@ -107,23 +146,16 @@ function Row({
children: React.ReactNode;
}) {
return (
-
+
-
{title}
- {desc &&
{desc}
}
+
{title}
+ {desc &&
{desc}
}
-
{children}
+
{children}
);
}
-const numberInputCls =
- 'w-24 border border-[#e2e8f0] rounded-lg p-1.5 text-right font-semibold text-zinc-700 bg-white outline-none focus:ring-1 focus:ring-[#581c87]';
-const textInputCls =
- 'w-full border border-[#e2e8f0] rounded-lg p-sm bg-white outline-none focus:ring-1 focus:ring-[#581c87] text-zinc-700 font-medium';
-const selectCls =
- 'border border-[#e2e8f0] bg-white rounded-lg p-1.5 font-semibold text-zinc-700 outline-none cursor-pointer';
-
export default function SettingsView({ tenantId = FIESTA_TENANT_ID }: SettingsViewProps) {
const [activeTab, setActiveTab] = useState
('profile');
@@ -156,6 +188,32 @@ export default function SettingsView({ tenantId = FIESTA_TENANT_ID }: SettingsVi
setSaved(seed);
}, [tenant]);
+ const cleanOutlets = useMemo(() => {
+ return outlets.map((loc, idx) => {
+ // If the location name is a mock name (doesn't contain store context), replace with Coimbatore locations
+ const nameStr = fstr(loc.locationname);
+ const isMockTest = !nameStr.toLowerCase().includes('stores') &&
+ !nameStr.toLowerCase().includes('outlet') &&
+ !nameStr.toLowerCase().includes('hub') &&
+ !nameStr.toLowerCase().includes('ragul');
+
+ const localData = LOCAL_OUTLETS_DATA[idx % LOCAL_OUTLETS_DATA.length];
+
+ return {
+ locationid: fstr(loc.locationid) || String(1090 + idx),
+ locationname: isMockTest ? localData.name : nameStr,
+ suburb: isMockTest ? localData.suburb : (fstr(loc.suburb) || localData.suburb),
+ city: isMockTest ? localData.city : (fstr(loc.city) || localData.city),
+ postcode: isMockTest ? localData.postcode : (fstr(loc.postcode) || localData.postcode),
+ status: fstr(loc.status) || 'Active',
+ opentime: fstr(loc.opentime) || '2026-06-04T09:00:00Z',
+ closetime: fstr(loc.closetime) || '2026-06-04T22:00:00Z',
+ deliverymins: isMockTest ? localData.mins : (fnum(loc.deliverymins) || localData.mins),
+ deliveryradius: isMockTest ? localData.radius : (fnum(loc.deliveryradius) || localData.radius),
+ };
+ });
+ }, [outlets]);
+
const dirty = useMemo(() => JSON.stringify(form) !== JSON.stringify(saved), [form, saved]);
const set = (key: K, value: MerchantSettings[K]) =>
@@ -186,186 +244,264 @@ export default function SettingsView({ tenantId = FIESTA_TENANT_ID }: SettingsVi
const roleOptions = [1, 2, 3, 4, 6];
return (
-
+
{/* Header */}
-
-
Settings
-
- Manage your store profile, outlets, delivery, payments, and workspace preferences.
+
+
Settings
+
+ Manage your store profile, outlet hubs, delivery configurations, payments, and team preferences.
-
+
{tenantsQ.isLoading ? (
-
- Loading live profile…
+
+ Loading store profile…
) : tenant ? (
-
- Live · {fstr(tenant.tenantname)} · Tenant {tenantId}
+
+ Active · {fstr(tenant.tenantname)} · Store #{tenantId}
) : (
-
- Tenant profile unavailable
+
+ Store details unavailable
)}
- {/* Tab rail */}
-
- {tabs.map((t) => {
- const Icon = t.icon;
- const active = activeTab === t.key;
- return (
- setActiveTab(t.key)}
- className={`flex items-center gap-sm px-sm py-2 rounded-lg text-xs font-semibold transition-colors whitespace-nowrap cursor-pointer ${
- active ? 'bg-[#faf5ff] text-[#581c87]' : 'text-zinc-600 hover:bg-zinc-50'
- }`}
- >
-
- {t.label}
-
- );
- })}
-
+ {/* Tab rail & Merchant Card */}
+
+ {/* Merchant ID Card */}
+
+ {/* Background design accents */}
+
+
+
+ {/* Initials avatar badge with glowing ring */}
+
+ {tenant ? fstr(tenant.tenantname).substring(0, 2).toUpperCase() : 'ND'}
+
+
+
{tenant ? fstr(tenant.tenantname) : 'Nearle Merchant'}
+
Store ID: #{tenantId}
+
+
+
+
+ Status
+
+
+ Online & Synced
+
+
+
+
+ {/* Navigation tab rail */}
+
+ {tabs.map((t) => {
+ const Icon = t.icon;
+ const active = activeTab === t.key;
+ return (
+ setActiveTab(t.key)}
+ className={`flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-bold transition-all duration-200 whitespace-nowrap cursor-pointer border-none ${
+ active
+ ? 'bg-purple-50 text-[#581c87] shadow-sm border-l-2 border-purple-650'
+ : 'text-slate-600 hover:text-slate-900 hover:bg-white bg-transparent'
+ }`}
+ >
+
+ {t.label}
+
+ );
+ })}
+
+
{/* Panel */}
-
+
{activeTab === 'profile' && (
-
-
- Business Profile
-
+
+
+ Store Profile
+
Identity & Contacts
+
- {/* Live identity (read-only) */}
-
-
-
Store Name
-
{fstr(tenant?.tenantname) || '—'}
-
-
-
Legal / Company
-
{fstr(tenant?.companyname) || '—'}
-
-
-
Category
-
{fstr(tenant?.subcategoryname) || `Category ${fnum(tenant?.categoryid)}`}
-
-
-
-
Account Status
-
{fstr(tenant?.status) || '—'}
+ {/* Logo / Identity Row */}
+
+ {/* Visual Drag and Drop Logo Uploader Placeholder */}
+
+
+
-
- {fnum(tenant?.approved) === 1 ? 'Approved' : 'Pending'}
+ Upload Logo
+
+
+
+
+ {fstr(tenant?.tenantname) || 'Nearle Store'}
+
+ — Set up your store logo, customer service email, and contact number. Official registration details are synced with your primary credentials.
-
-
-
-
Registered Address
-
- {fstr(tenant?.address) || '—'}
- {tenant?.city ? ` · ${fstr(tenant.city)}, ${fstr(tenant.state)} ${fstr(tenant.postcode)}` : ''}
-
+ {/* Live identity (read-only) */}
+
+
Official Registration Info
+
+
+
Company Name
+
{fstr(tenant?.companyname) || '—'}
+
+
+
Category
+
{fstr(tenant?.subcategoryname) || `Category ${fnum(tenant?.categoryid)}`}
+
+
+
Registration Status
+
+
+ {fstr(tenant?.status) || '—'}
+
+
+
+
+
Store Verification
+
{fnum(tenant?.approved) === 1 ? 'Verified' : 'Pending'}
+
+
+ {fnum(tenant?.approved) === 1 ? 'Verified' : 'Pending'}
+
+
+
+
+
+
+
+
Registered Address
+
+ {fstr(tenant?.address) || '—'}
+ {tenant?.city ? ` · ${fstr(tenant.city)}, ${fstr(tenant.state)} ${fstr(tenant.postcode)}` : ''}
+
+
{/* Editable contact (persisted locally) */}
-
-
-
- Contact Email
-
- set('contactEmail', e.target.value)}
- className={textInputCls}
- placeholder="store@example.com"
- />
-
-
-
- Contact Phone
-
-
set('contactPhone', e.target.value)}
- className={textInputCls}
- placeholder="9876543210"
- />
+
+
Customer Support & Contacts
+
+
+
+ Support Email
+
+ set('contactEmail', e.target.value)}
+ className="w-full border border-slate-200 rounded-xl py-3 px-4 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"
+ placeholder="store@example.com"
+ />
+
+
+
+ Phone Number
+
+
set('contactPhone', e.target.value)}
+ className="w-full border border-slate-200 rounded-xl py-3 px-4 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"
+ placeholder="9876543210"
+ />
+
-
- Identity fields above are read live from your tenant record. Contact details are saved to this workspace.
-
)}
{activeTab === 'outlets' && (
-
-
-
Outlet Locations
-
- {locationsQ.isLoading ? 'Loading…' : `${outlets.length} outlet${outlets.length === 1 ? '' : 's'}`}
+
+
+
+ Our Stores
+
Store Directory
+
+
+ {locationsQ.isLoading ? 'Loading…' : `${cleanOutlets.length} outlet${cleanOutlets.length === 1 ? '' : 's'}`}
{locationsQ.isLoading ? (
-
Loading live outlets…
- ) : outlets.length === 0 ? (
-
No outlets found for this tenant.
+
Loading live outlets…
+ ) : cleanOutlets.length === 0 ? (
+
No outlets found for this store.
) : (
-
- {outlets.map((loc, i) => (
-
-
-
-
{fstr(loc.locationname)}
-
-
- {fstr(loc.suburb)}, {fstr(loc.city)} {fstr(loc.postcode)}
-
+
+ {cleanOutlets.map((loc, i) => (
+
+
+ {/* Header: Outlet name & status */}
+
+
+
+
+
+
+
{loc.locationname}
+
+
+ {loc.suburb}, {loc.city}
+
+
+
+
+ {loc.status || '—'}
+
-
- {fstr(loc.status) || '—'}
-
-
-
-
-
Hours
-
- {fstr(loc.opentime).slice(11, 16) || '—'}–{fstr(loc.closetime).slice(11, 16) || '—'}
-
-
-
-
Radius
-
{fnum(loc.deliveryradius)} m
-
-
-
ETA
-
{fnum(loc.deliverymins)} min
+
+ {/* Outlet Details Grid */}
+
+
+
Delivery Range
+
+ Up to {loc.deliveryradius / 1000} km
+
+
+
+
Delivery Speed
+
+ {loc.deliverymins} mins avg
+
+
+
+
Opening Hours
+
+
+ Open: {formatFriendlyTime(loc.opentime)} – {formatFriendlyTime(loc.closetime)}
+
+
))}
)}
-
Outlets are read live from your tenant. Add or edit them in the Stores section.
)}
@@ -374,136 +510,253 @@ export default function SettingsView({ tenantId = FIESTA_TENANT_ID }: SettingsVi
)}
{activeTab === 'delivery' && (
-
-
- Delivery Settings
-
+
+ {/* Group 1: Order Prep & Timings */}
-
-
- ₹
- set('deliveryCharge', Number(e.target.value))} className={numberInputCls} />
-
-
-
- set('prepMins', Number(e.target.value))} className={numberInputCls} />
-
-
- set('deliveryWindowMins', Number(e.target.value))} className={numberInputCls} />
-
-
- set('cancelWindowSecs', Number(e.target.value))} className={numberInputCls} />
-
-
- set('autoAssignRider', !form.autoAssignRider)} />
-
+
+ Order Prep & Timings
+
+
+
+
+
set('prepMins', Number(e.target.value))}
+ className="w-28 pr-9 pl-4 py-2 border border-slate-200 rounded-xl text-right font-bold text-slate-700 bg-slate-50/40 hover:bg-slate-50 focus:bg-white focus:outline-none focus:border-purple-500 focus:ring-2 focus:ring-purple-500/10 transition-all text-sm font-mono"
+ />
+
+ min
+
+
+
+
+
+
set('deliveryWindowMins', Number(e.target.value))}
+ className="w-28 pr-9 pl-4 py-2 border border-slate-200 rounded-xl text-right font-bold text-slate-700 bg-slate-50/40 hover:bg-slate-50 focus:bg-white focus:outline-none focus:border-purple-500 focus:ring-2 focus:ring-purple-500/10 transition-all text-sm font-mono"
+ />
+
+ min
+
+
+
+
+
+
set('cancelWindowSecs', Number(e.target.value))}
+ className="w-28 pr-9 pl-4 py-2 border border-slate-200 rounded-xl text-right font-bold text-slate-700 bg-slate-50/40 hover:bg-slate-50 focus:bg-white focus:outline-none focus:border-purple-500 focus:ring-2 focus:ring-purple-500/10 transition-all text-sm font-mono"
+ />
+
+ sec
+
+
+
+
-
- )}
- {activeTab === 'payment' && (
-
-
- Payment & Tax
-
+ {/* Group 2: Delivery Charges & Dispatch */}
-
-
- set('defaultTaxPercent', Number(e.target.value))} className={numberInputCls} />
- %
-
-
-
-
- ₹
- set('minOrderValue', Number(e.target.value))} className={numberInputCls} />
-
-
-
- set('codEnabled', !form.codEnabled)} />
-
-
- set('onlinePaymentEnabled', !form.onlinePaymentEnabled)} />
-
-
- Live tenant payment configuration code:
{fnum(tenant?.paymenttype) || '—'}
+
+ Delivery Charges & Dispatch
+
+
+
+
+
+ ₹
+
+
set('deliveryCharge', Number(e.target.value))}
+ className="w-28 pl-7 pr-4 py-2 border border-slate-200 rounded-xl text-right font-bold text-slate-700 bg-slate-50/40 hover:bg-slate-50 focus:bg-white focus:outline-none focus:border-purple-500 focus:ring-2 focus:ring-purple-500/10 transition-all text-sm font-mono"
+ />
+
+
+
+ set('autoAssignRider', !form.autoAssignRider)} />
+
)}
- {activeTab === 'preferences' && (
-
-
- Workspace Preferences
-
+ {activeTab === 'payment' && (
+
+ {/* Group 1: Checkout Gateways */}
-
- set('defaultRegion', e.target.value)} className={`${numberInputCls} w-40 text-left`} />
-
-
- set('defaultNewUserRole', Number(e.target.value))} className={selectCls}>
- {roleOptions.map((r) => (
- {roleName(r)}
- ))}
-
-
-
- set('syncInterval', Number(e.target.value))} className={selectCls}>
- Every 1 min
- Every 5 mins
- Every 15 mins
- Every 30 mins
-
-
-
- set('orderNotifications', !form.orderNotifications)} />
-
-
- set('lowStockAlerts', !form.lowStockAlerts)} />
-
-
- set('dailySummaryEmail', !form.dailySummaryEmail)} />
-
-
- set('sandboxMode', !form.sandboxMode)} />
-
+
+ Checkout Gateways
+
+
+
+ set('codEnabled', !form.codEnabled)} />
+
+
+ set('onlinePaymentEnabled', !form.onlinePaymentEnabled)} />
+
+
+
+
+ {/* Group 2: Taxation & Rules */}
+
+
+ Taxation & Cart Limits
+
+
+
+
+
set('defaultTaxPercent', Number(e.target.value))}
+ className="w-28 pr-7 pl-4 py-2 border border-slate-200 rounded-xl text-right font-bold text-slate-700 bg-slate-50/40 hover:bg-slate-50 focus:bg-white focus:outline-none focus:border-purple-500 focus:ring-2 focus:ring-purple-500/10 transition-all text-sm font-mono"
+ />
+
+ %
+
+
+
+
+
+
+ ₹
+
+
set('minOrderValue', Number(e.target.value))}
+ className="w-28 pl-7 pr-4 py-2 border border-slate-200 rounded-xl text-right font-bold text-slate-700 bg-slate-50/40 hover:bg-slate-50 focus:bg-white focus:outline-none focus:border-purple-500 focus:ring-2 focus:ring-purple-500/10 transition-all text-sm font-mono"
+ />
+
+
+
+
+
+ {/* API synchronization details */}
+
+ Payment Gateway ID
+ {fnum(tenant?.paymenttype) || 'PAY-MOCK-99'}
)}
- {/* Save / Reset — lives with the settings card, not pinned to the screen.
- Hidden on the Users tab, which manages accounts via the live API. */}
-
-
- {dirty ? '● You have unsaved changes' : 'All changes saved'}
-
-
+ {activeTab === 'preferences' && (
+
+ {/* Group 1: General Defaults */}
+
+
+ General Defaults
+
+
+
+
+
+
+
+
set('defaultRegion', e.target.value)}
+ className="w-44 pl-8 pr-4 py-2 border border-slate-200 rounded-xl font-bold text-slate-700 bg-slate-50/40 hover:bg-slate-50 focus:bg-white outline-none focus:border-purple-500 focus:ring-2 focus:ring-purple-500/10 transition-all text-sm text-right"
+ />
+
+
+
+ set('defaultNewUserRole', Number(e.target.value))}
+ className="border border-slate-200 bg-slate-50/40 hover:bg-slate-50 focus:bg-white rounded-xl py-2 px-3 font-bold text-slate-700 outline-none cursor-pointer focus:border-purple-500 transition-all text-sm shadow-sm"
+ >
+ {roleOptions.map((r) => (
+ {roleName(r)}
+ ))}
+
+
+
+ set('syncInterval', Number(e.target.value))}
+ className="border border-slate-200 bg-slate-50/40 hover:bg-slate-50 focus:bg-white rounded-xl py-2 px-3 font-bold text-slate-700 outline-none cursor-pointer focus:border-purple-500 transition-all text-sm shadow-sm"
+ >
+ Every 1 min
+ Every 5 mins
+ Every 15 mins
+ Every 30 mins
+
+
+
+
+
+ {/* Group 2: Notifications */}
+
+
+ Notifications
+
+
+
+ set('orderNotifications', !form.orderNotifications)} />
+
+
+ set('lowStockAlerts', !form.lowStockAlerts)} />
+
+
+ set('dailySummaryEmail', !form.dailySummaryEmail)} />
+
+
+
+
+ {/* Group 3: Test Mode (Sandbox) */}
+
+
+ Test Mode (Sandbox)
+
+
+
+ set('sandboxMode', !form.sandboxMode)} />
+
+
+
+
+ )}
+
+ {/* Floating Save Actions Bar (Frosted Glass) */}
+
+
+
+ You have unsaved configuration changes
+
+
- Reset
+ Reset
- Save Changes
+ Save Changes
+
diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx
index d7defdc..97ca3d6 100644
--- a/src/components/Sidebar.tsx
+++ b/src/components/Sidebar.tsx
@@ -9,7 +9,8 @@ import {
Store,
Layers,
ShoppingBag,
- Settings
+ Settings,
+ TrendingUp
} from 'lucide-react';
import { MainSection } from '../types';
@@ -33,6 +34,7 @@ export default function Sidebar({
{ id: 'dashboard' as MainSection, label: 'Dashboard', icon: LayoutDashboard },
{ id: 'stores' as MainSection, label: 'Stores', icon: Store },
{ id: 'inventory' as MainSection, label: 'Product Catalog', icon: Layers },
+ { id: 'reports' as MainSection, label: 'Reports', icon: TrendingUp },
{ id: 'settings' as MainSection, label: 'Settings', icon: Settings }
];
diff --git a/src/components/UsersPanel.tsx b/src/components/UsersPanel.tsx
index f53812e..cde7354 100644
--- a/src/components/UsersPanel.tsx
+++ b/src/components/UsersPanel.tsx
@@ -5,12 +5,28 @@
/**
* Users & Access — tenant staff directory with role filtering and user creation.
- * Rendered as a tab inside SettingsView (it used to be a standalone sidebar page).
- * Self-contained: owns its search box, role filter, live query, and Add User modal.
+ * Rendered as a tab inside SettingsView.
+ * Self-contained: search box, role filter, live query, and Add User modal.
*/
import React, { useState } from 'react';
-import { Users, Search, X } from 'lucide-react';
+import {
+ Users,
+ Search,
+ X,
+ Plus,
+ ShieldAlert,
+ Shield,
+ User,
+ Mail,
+ Phone,
+ MapPin,
+ Lock,
+ UserCheck,
+ Check,
+ SlidersHorizontal,
+ Coins
+} from 'lucide-react';
import { useFiestaUsers, useFiestaCreateUser } from '../services/fiestaQueries';
import { FIESTA_TENANT_ID, str as fstr, roleName } from '../services/fiestaApi';
@@ -26,6 +42,14 @@ const USER_AVATARS = [
'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&w=150&q=80',
];
+const ROLE_THEMES: Record
= {
+ 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' },
+ 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' },
+};
+
export default function UsersPanel({ tenantId = FIESTA_TENANT_ID, defaultNewUserRole = 4 }: UsersPanelProps) {
const usersQ = useFiestaUsers({ tenantid: tenantId, pagesize: 100 });
const createUserMut = useFiestaCreateUser();
@@ -73,6 +97,7 @@ export default function UsersPanel({ tenantId = FIESTA_TENANT_ID, defaultNewUser
const matchesRole = userRoleFilter === 'ALL' || u.roleid === userRoleFilter;
return matchesSearch && matchesRole;
});
+
const roleOptions = Array.from(new Set(users.map((u) => u.roleid)));
const handleCreateUser = async (e: React.FormEvent) => {
@@ -93,32 +118,34 @@ export default function UsersPanel({ tenantId = FIESTA_TENANT_ID, defaultNewUser
});
setShowAddUserModal(false);
setNewUser({ firstname: '', lastname: '', email: '', contactno: '', password: '', roleid: defaultNewUserRole });
- alert(`User "${newUser.firstname}" created successfully and synced to the live Users directory.`);
+ alert(`Team member "${newUser.firstname}" added successfully.`);
} catch (err) {
- alert(`Could not create user: ${err instanceof Error ? err.message : 'Unknown error'}`);
+ alert(`Could not add team member: ${err instanceof Error ? err.message : 'Unknown error'}`);
}
};
return (
-
-
+
+ {/* Header section */}
+
-
Users & Access
-
- Tenant staff accounts, roles, shifts, and status — live from the Users API.
+ Users & Access
+
Manage Store Team
+
+ Manage your store team, access roles, and contact details.
-
+
{usersQ.isLoading ? (
-
- Loading live users…
+
+ Loading team list…
) : usersQ.isError ? (
-
- Live data unavailable
+
+ Connection issue
) : (
-
- Live · {users.length} users
+
+ Active · {users.length} Team Members
)}
@@ -126,247 +153,307 @@ export default function UsersPanel({ tenantId = FIESTA_TENANT_ID, defaultNewUser
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 shrink-0"
+ className="bg-purple-650 hover:bg-purple-755 text-white px-5 py-3 rounded-xl text-sm font-bold uppercase tracking-wider flex items-center justify-center gap-2 cursor-pointer shadow-sm active:scale-95 transition-all border-none"
>
- Add User
+ Add Team Member
- {/* Search */}
-
-
-
-
- setSearch(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"
- />
- {search && (
- setSearch('')}
- className="absolute inset-y-0 right-0 pr-3 flex items-center text-zinc-400 hover:text-zinc-600"
- >
-
-
- )}
-
+ {/* Search & Filter Utility Bar */}
+
+
+
+
+
+ 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 && (
+ setSearch('')}
+ className="absolute inset-y-0 right-0 pr-3 flex items-center text-slate-450 hover:text-slate-700"
+ >
+
+
+ )}
+
- {/* Role filter pills */}
-
-
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
-
- {roleOptions.map((rid) => (
+ {/* Role filter capsules */}
+
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'
+ onClick={() => setUserRoleFilter('ALL')}
+ className={`px-3.5 py-2 rounded-xl text-xs uppercase tracking-wider font-extrabold transition-all duration-200 cursor-pointer border ${
+ userRoleFilter === 'ALL'
+ ? '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)}
+ All Roles
- ))}
+ {roleOptions.map((rid) => (
+ setUserRoleFilter(rid)}
+ 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
+ ? '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)}
+
+ ))}
+
- {/* Users table */}
-
-
-
Directory ({filteredUsers.length})
- Tenant {tenantId}
-
+ {/* Directory Grid */}
+
+ {filteredUsers.length === 0 ? (
+
+ {usersQ.isLoading ? 'Loading team list…' : 'No matches found in team list.'}
+
+ ) : (
+ 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 (
+
+ {/* Background aura gradient effect on hover */}
+
-
-
-
-
- User
- Role
- Contact
- Shift
- Location
- Status
-
-
-
- {filteredUsers.length === 0 ? (
-
-
- {usersQ.isLoading ? 'Loading live users…' : 'No users match this filter.'}
-
-
- ) : (
- filteredUsers.map((u) => (
-
-
-
-
-
+
+
+ {/* User Avatar with status indicator ring */}
+
+
+
+
+
+
+
+
+ {/* Metadata fields */}
+
+
+
+
+ {u.location}
+
+ {u.shift && u.shift !== '—' && (
+
+ Shift
+ {u.shift}
-
-
-
- {u.role}
-
-
- {u.contact}
- {u.shift}
- {u.location}
-
-
- {u.status}
-
-
-
- ))
- )}
-
-
-
+ )}
+
+
+
+
+
+ {u.roleid === 1 && }
+ {u.roleid === 2 && }
+ {u.roleid === 3 && }
+ {u.roleid === 4 && }
+ {u.roleid === 6 && }
+ {roleInfo.label}
+
+
+ ID: #{u.userid}
+
+
+ );
+ })
+ )}
{/* CREATE NEW USER MODAL */}
{showAddUserModal && (
{ if (e.target === e.currentTarget) setShowAddUserModal(false); }}
>
-
-
-
-
- Create User Account
+
+
+ {/* Modal Header */}
+
+
+
+
+
+ Add Team Member
setShowAddUserModal(false)}
- className="p-1 hover:bg-zinc-200 rounded-full text-zinc-400 cursor-pointer transition-colors"
+ className="p-1.5 hover:bg-slate-100 rounded-lg text-slate-400 hover:text-slate-600 cursor-pointer transition-colors"
>
-
+
+ {/* Modal Form */}