661 lines
32 KiB
TypeScript
661 lines
32 KiB
TypeScript
/**
|
||
* @license
|
||
* SPDX-License-Identifier: Apache-2.0
|
||
*/
|
||
|
||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||
import {
|
||
Building2,
|
||
Store,
|
||
Truck,
|
||
CreditCard,
|
||
SlidersHorizontal,
|
||
Users,
|
||
MapPin,
|
||
Phone,
|
||
Mail,
|
||
Plus,
|
||
Bike
|
||
} from 'lucide-react';
|
||
import { useFiestaAllTenants, useFiestaTenantLocations, useFiestaUpdateLocation, useFiestaDeleteLocation } from '../services/fiestaQueries';
|
||
import { useAppRoles } from '../services/queries';
|
||
import { FIESTA_TENANT_ID, str as fstr, num as fnum, roleName } from '../services/fiestaApi';
|
||
import UsersPanel from './UsersPanel';
|
||
import AwaitingApi from './AwaitingApi';
|
||
import AdminConsole from './AdminConsole';
|
||
import { ConfirmModal } from './ConfirmModal';
|
||
import type { AuthUser } from '../services/auth';
|
||
|
||
type TabKey = 'profile' | 'outlets' | 'users';
|
||
|
||
/** Locally-persisted merchant preferences (survive reload via localStorage). */
|
||
interface MerchantSettings {
|
||
// Business profile (seeded from live tenant data, then locally editable)
|
||
contactEmail: string;
|
||
contactPhone: string;
|
||
minOrderValue: number;
|
||
// Delivery
|
||
deliveryCharge: number;
|
||
prepMins: number;
|
||
deliveryWindowMins: number;
|
||
cancelWindowSecs: number;
|
||
autoAssignRider: boolean;
|
||
// Payment & tax
|
||
defaultTaxPercent: number;
|
||
codEnabled: boolean;
|
||
onlinePaymentEnabled: boolean;
|
||
// Preferences
|
||
defaultRegion: string;
|
||
defaultNewUserRole: number;
|
||
orderNotifications: boolean;
|
||
lowStockAlerts: boolean;
|
||
dailySummaryEmail: boolean;
|
||
syncInterval: number;
|
||
sandboxMode: boolean;
|
||
}
|
||
|
||
const DEFAULTS: MerchantSettings = {
|
||
contactEmail: '',
|
||
contactPhone: '',
|
||
minOrderValue: 0,
|
||
deliveryCharge: 30,
|
||
prepMins: 15,
|
||
deliveryWindowMins: 45,
|
||
cancelWindowSecs: 60,
|
||
autoAssignRider: true,
|
||
defaultTaxPercent: 5,
|
||
codEnabled: true,
|
||
onlinePaymentEnabled: true,
|
||
defaultRegion: 'Coimbatore',
|
||
defaultNewUserRole: 4,
|
||
orderNotifications: true,
|
||
lowStockAlerts: true,
|
||
dailySummaryEmail: false,
|
||
syncInterval: 5,
|
||
sandboxMode: false,
|
||
};
|
||
|
||
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 Row({
|
||
title,
|
||
desc,
|
||
children,
|
||
}: {
|
||
title: string;
|
||
desc?: string;
|
||
children: React.ReactNode;
|
||
}) {
|
||
return (
|
||
<div className="flex justify-between items-center gap-5 py-5 px-4 bg-white hover:bg-slate-50/20 border-b border-slate-100/70 last:border-none transition-all duration-200">
|
||
<div className="min-w-0">
|
||
<h4 className="font-sans font-bold text-sm text-slate-800 leading-tight">{title}</h4>
|
||
{desc && <p className="text-slate-500 text-xs mt-1.5 font-medium leading-relaxed">{desc}</p>}
|
||
</div>
|
||
<div className="shrink-0 flex items-center">{children}</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export interface SettingsViewProps {
|
||
tenantId?: number;
|
||
user?: AuthUser;
|
||
}
|
||
|
||
export default function SettingsView({ tenantId = FIESTA_TENANT_ID, user }: SettingsViewProps) {
|
||
const [activeTab, setActiveTab] = useState<TabKey>('profile');
|
||
|
||
// Live tenant profile + outlets.
|
||
// Fetch a larger page size to ensure we find our specific tenant (ID 1087).
|
||
const tenantsQ = useFiestaAllTenants({ pagesize: 5000, status: '' });
|
||
const tenant = (tenantsQ.data ?? []).find((t) => Number(t.tenantid) === tenantId) || null;
|
||
const locationsQ = useFiestaTenantLocations(tenantId);
|
||
const outlets = locationsQ.data ?? [];
|
||
|
||
// Application roles (Hasura) — drives the role dropdowns.
|
||
const rolesQ = useAppRoles();
|
||
|
||
// In-session workspace preferences. These have NO merchant-settings backend
|
||
// (see [R6]) so they are not persisted; the operational controls that would
|
||
// need persistence show an AwaitingApi notice instead of saving silently.
|
||
const [form, setForm] = useState<MerchantSettings>({ ...DEFAULTS });
|
||
|
||
const updateLocationMut = useFiestaUpdateLocation();
|
||
const deleteLocationMut = useFiestaDeleteLocation();
|
||
const [editingStore, setEditingStore] = useState<any>(null);
|
||
const [showEditStoreModal, setShowEditStoreModal] = useState(false);
|
||
const [confirmModalState, setConfirmModalState] = useState<{isOpen: boolean, loc: any}>({ isOpen: false, loc: null });
|
||
|
||
const handleUpdateStoreSubmit = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
if (!editingStore) return;
|
||
try {
|
||
await updateLocationMut.mutateAsync({
|
||
tenantid: Number(tenantId),
|
||
locationid: Number(editingStore.locationid),
|
||
locationname: editingStore.locationname,
|
||
suburb: editingStore.suburb,
|
||
city: editingStore.city,
|
||
contactno: editingStore.contactno,
|
||
status: editingStore.status,
|
||
});
|
||
setShowEditStoreModal(false);
|
||
setEditingStore(null);
|
||
alert('Outlet updated successfully!');
|
||
} catch (err: any) {
|
||
alert(err.message || 'Failed to update outlet.');
|
||
}
|
||
};
|
||
|
||
const handleDeleteStore = async (loc: any) => {
|
||
setConfirmModalState({ isOpen: true, loc });
|
||
};
|
||
|
||
const confirmDeleteOutlet = async () => {
|
||
const loc = confirmModalState.loc;
|
||
if (!loc) return;
|
||
try {
|
||
await deleteLocationMut.mutateAsync({ tenantid: tenantId, locationid: loc.locationid });
|
||
setConfirmModalState({ isOpen: false, loc: null });
|
||
} catch (err: any) {
|
||
alert(err.message || 'Failed to delete outlet.');
|
||
setConfirmModalState({ isOpen: false, loc: null });
|
||
}
|
||
};
|
||
|
||
const [showStoreOnboarding, setShowStoreOnboarding] = useState(false);
|
||
|
||
useEffect(() => {
|
||
if (activeTab !== 'outlets') {
|
||
setShowStoreOnboarding(false);
|
||
}
|
||
}, [activeTab]);
|
||
|
||
// First-run seeding: fill region/role defaults from the live tenant once it
|
||
// arrives (used at runtime by the Add User dialog / region label).
|
||
const seededRef = useRef(false);
|
||
useEffect(() => {
|
||
if (seededRef.current || !tenant) return;
|
||
seededRef.current = true;
|
||
setForm((prev) => ({
|
||
...prev,
|
||
contactEmail: prev.contactEmail || fstr(tenant.primaryemail),
|
||
contactPhone: prev.contactPhone || fstr(tenant.primarycontact),
|
||
minOrderValue: prev.minOrderValue || fnum(tenant.minorder),
|
||
defaultRegion: prev.defaultRegion || fstr(tenant.city) || 'Coimbatore',
|
||
}));
|
||
}, [tenant]);
|
||
|
||
// Live outlets only — no fabricated fallback. Render whatever the API returns.
|
||
const cleanOutlets = useMemo(() => {
|
||
return outlets.map((loc, idx) => ({
|
||
locationid: fstr(loc.locationid) || String(idx),
|
||
locationname: fstr(loc.locationname) || '—',
|
||
suburb: fstr(loc.suburb),
|
||
city: fstr(loc.city),
|
||
postcode: fstr(loc.postcode),
|
||
status: fstr(loc.status) || '—',
|
||
opentime: fstr(loc.opentime),
|
||
closetime: fstr(loc.closetime),
|
||
deliverymins: fnum(loc.deliverymins),
|
||
deliveryradius: fnum(loc.deliveryradius),
|
||
}));
|
||
}, [outlets]);
|
||
|
||
const set = <K extends keyof MerchantSettings>(key: K, value: MerchantSettings[K]) =>
|
||
setForm((f) => ({ ...f, [key]: value }));
|
||
|
||
const tabs: Array<{ key: TabKey; label: string; icon: typeof Building2 }> = [
|
||
{ key: 'profile', label: 'Business Profile', icon: Building2 },
|
||
{ key: 'outlets', label: 'Outlets', icon: Store },
|
||
{ key: 'users', label: 'Users & Access', icon: Users },
|
||
];
|
||
|
||
// Build role options from the live app-roles API; fall back to the known
|
||
// numeric roles + roleName() helper when the API has no rows/names.
|
||
const roleOptions = useMemo<Array<{ id: number; name: string }>>(() => {
|
||
const rows = rolesQ.data ?? [];
|
||
const mapped = rows
|
||
.map((r) => {
|
||
const id = fnum((r as Record<string, unknown>).roleid);
|
||
const name =
|
||
fstr((r as Record<string, unknown>).rolename) ||
|
||
fstr((r as Record<string, unknown>).name) ||
|
||
roleName(id);
|
||
return { id, name };
|
||
})
|
||
.filter((r) => r.id > 0);
|
||
if (mapped.length) return mapped;
|
||
return [1, 2, 3, 4, 6].map((id) => ({ id, name: roleName(id) }));
|
||
}, [rolesQ.data]);
|
||
|
||
return (
|
||
<div className="font-sans text-slate-700">
|
||
{/* Header Removed */}
|
||
|
||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-gutter items-start">
|
||
{/* Tab rail & Merchant Card */}
|
||
<div className="lg:col-span-1 space-y-md bg-slate-50/50 border border-slate-200/60 p-4 shadow-sm">
|
||
{/* Merchant ID Card */}
|
||
<div className="bg-gradient-to-br from-slate-900 via-slate-950 to-purple-955 border border-purple-500/20 p-5 rounded-2xl text-white shadow-md relative overflow-hidden select-none">
|
||
{/* Background design accents */}
|
||
<div className="absolute top-0 right-0 w-28 h-28 bg-purple-500/10 rounded-full blur-xl -mr-8 -mt-8 pointer-events-none" />
|
||
|
||
<div className="relative z-10 flex items-center gap-3.5">
|
||
{/* Initials avatar badge with glowing ring */}
|
||
<div className="w-12 h-12 rounded-full bg-gradient-to-tr from-purple-500 to-indigo-650 border border-purple-400/30 flex items-center justify-center font-black text-sm shadow-[0_0_15px_rgba(168,85,247,0.35)] shrink-0">
|
||
{user?.name ? user.name.substring(0, 2).toUpperCase() : tenant ? fstr(tenant.tenantname).substring(0, 2).toUpperCase() : 'ND'}
|
||
</div>
|
||
<div className="min-w-0">
|
||
<h4 className="font-sans font-bold text-sm truncate text-white">{user?.name || (tenant ? fstr(tenant.tenantname) : 'Nearle Merchant')}</h4>
|
||
<p className="text-slate-400 text-[10px] font-mono mt-1 truncate uppercase tracking-wider">Store ID: #{tenantId}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="mt-5 pt-4 border-t border-slate-800/80 flex justify-between items-center text-xs">
|
||
<span className="text-slate-400 font-medium">Status</span>
|
||
<span className="inline-flex items-center gap-1.5 font-bold text-emerald-455">
|
||
<span className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse" />
|
||
Online & Synced
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Navigation tab rail */}
|
||
<nav className="flex lg:flex-col gap-1.5 overflow-x-auto custom-scrollbar no-scrollbar touch-scrolling select-none">
|
||
{tabs.map((t) => {
|
||
const Icon = t.icon;
|
||
const active = activeTab === t.key;
|
||
return (
|
||
<button
|
||
key={t.key}
|
||
onClick={() => 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-[#662582] shadow-sm border-l-2 border-purple-650'
|
||
: 'text-slate-600 hover:text-slate-900 hover:bg-white bg-transparent'
|
||
}`}
|
||
>
|
||
<Icon size={16} className={active ? 'text-[#662582]' : 'text-slate-450'} />
|
||
<span>{t.label}</span>
|
||
</button>
|
||
);
|
||
})}
|
||
</nav>
|
||
</div>
|
||
|
||
{/* Panel */}
|
||
<div className="lg:col-span-3 space-y-gutter text-sm pb-24">
|
||
{activeTab === 'profile' && (
|
||
<div className="bg-white border border-slate-200/60 p-6 shadow-sm space-y-lg animate-in fade-in duration-200">
|
||
<div>
|
||
<span className="text-xs font-bold text-slate-450 uppercase tracking-widest block">Store Profile</span>
|
||
<h2 className="text-xl font-bold text-slate-900 mt-1">Identity & Contacts</h2>
|
||
</div>
|
||
|
||
{/* Identity Row */}
|
||
<div className="flex flex-col pb-6 border-b border-slate-100">
|
||
<div className="min-w-0 flex-1 leading-relaxed">
|
||
<span className="text-base font-bold text-slate-900 mr-2">{user?.name || fstr(tenant?.tenantname) || 'Nearle Store'}</span>
|
||
<span className="text-slate-500 text-xs font-medium">
|
||
— View your customer service email and contact number. Official registration details are synced with your primary credentials.
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Live identity (read-only) */}
|
||
<div className="space-y-sm bg-slate-50/50 p-5 rounded-2xl border border-slate-100/80">
|
||
<span className="text-xs font-bold text-slate-500 uppercase tracking-wider block">Official Registration Info</span>
|
||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-md mt-3">
|
||
<div className="p-4 bg-white rounded-xl border border-slate-200/60 shadow-sm">
|
||
<span className="text-[10px] text-slate-400 uppercase font-black tracking-wider block">Company Name</span>
|
||
<p className="font-bold text-slate-800 text-sm mt-1">{fstr(tenant?.companyname) || user?.name || 'Nearle Merchant'}</p>
|
||
</div>
|
||
<div className="p-4 bg-white rounded-xl border border-slate-200/60 shadow-sm">
|
||
<span className="text-[10px] text-slate-400 uppercase font-black tracking-wider block">Category</span>
|
||
<p className="font-bold text-slate-800 text-sm mt-1">{fstr(tenant?.subcategoryname) || (tenant?.categoryid ? `Category ${fnum(tenant.categoryid)}` : 'General Retail')}</p>
|
||
</div>
|
||
<div className="p-4 bg-white rounded-xl border border-slate-200/60 shadow-sm">
|
||
<span className="text-[10px] text-slate-400 uppercase font-black tracking-wider block">Registration Status</span>
|
||
<p className="font-bold text-slate-800 text-sm mt-1 flex items-center gap-1.5">
|
||
<span className={`w-2 h-2 rounded-full ${
|
||
(fstr(tenant?.status).toLowerCase() === 'active' || user) ? 'bg-emerald-500' : 'bg-slate-400'
|
||
}`} />
|
||
{fstr(tenant?.status) || (user ? 'Active' : '—')}
|
||
</p>
|
||
</div>
|
||
<div className="p-4 bg-white rounded-xl border border-slate-200/60 shadow-sm flex items-center justify-between">
|
||
<div>
|
||
<span className="text-[10px] text-slate-400 uppercase font-black tracking-wider block">Store Verification</span>
|
||
<p className="font-bold text-slate-800 text-sm mt-1">{fnum(tenant?.approved) === 1 || user ? 'Verified' : 'Pending'}</p>
|
||
</div>
|
||
<span className={`px-2.5 py-1 rounded-lg text-[9px] font-black uppercase tracking-wider ${
|
||
fnum(tenant?.approved) === 1 || user
|
||
? 'text-emerald-700 bg-emerald-50 border border-emerald-100/50'
|
||
: 'text-zinc-555 bg-zinc-100'
|
||
}`}>
|
||
{fnum(tenant?.approved) === 1 || user ? 'Verified' : 'Pending'}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="p-4 bg-white rounded-xl border border-slate-200/60 shadow-sm flex items-start gap-3 mt-4">
|
||
<MapPin size={18} className="text-slate-400 shrink-0 mt-0.5" />
|
||
<div>
|
||
<span className="text-[10px] text-slate-400 uppercase font-black tracking-wider block">Registered Address</span>
|
||
<p className="text-slate-700 font-semibold text-xs mt-1 leading-relaxed">
|
||
{fstr(tenant?.address) || [user?.locationname, user?.applocation].filter(Boolean).join(', ') || 'Headquarters'}
|
||
{tenant?.city ? ` · ${fstr(tenant.city)}, ${fstr(tenant.state)} ${fstr(tenant.postcode)}` : ''}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Customer support contacts — live (read-only) tenant values. */}
|
||
<div className="space-y-sm">
|
||
<span className="text-xs font-bold text-slate-500 uppercase tracking-wider block">Customer Support & Contacts</span>
|
||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-md mt-2">
|
||
<div className="space-y-1.5">
|
||
<label className="font-bold text-slate-500 uppercase tracking-widest text-[10px] flex items-center gap-1.5">
|
||
<Mail size={12} className="text-slate-400" /> Support Email
|
||
</label>
|
||
<input
|
||
type="email"
|
||
value={user?.email || fstr(tenant?.primaryemail) || ''}
|
||
readOnly
|
||
className="w-full border border-slate-200 rounded-xl py-3 px-4 bg-slate-50/40 outline-none transition-all text-slate-800 font-semibold text-sm shadow-sm"
|
||
placeholder="—"
|
||
/>
|
||
</div>
|
||
<div className="space-y-1.5">
|
||
<label className="font-bold text-slate-500 uppercase tracking-widest text-[10px] flex items-center gap-1.5">
|
||
<Phone size={12} className="text-slate-400" /> Phone Number
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={user?.contactno || fstr(tenant?.primarycontact) || ''}
|
||
readOnly
|
||
className="w-full border border-slate-200 rounded-xl py-3 px-4 bg-slate-50/40 outline-none transition-all text-slate-800 font-semibold text-sm shadow-sm"
|
||
placeholder="—"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{activeTab === 'outlets' && (
|
||
<div className="bg-white border border-slate-200/60 p-6 shadow-sm space-y-md animate-in fade-in duration-200">
|
||
<div className="flex justify-between items-center pb-4 border-b border-slate-100">
|
||
<div>
|
||
<span className="text-xs font-bold text-slate-450 uppercase tracking-widest block">
|
||
{showStoreOnboarding ? 'Onboarding' : 'Our Stores'}
|
||
</span>
|
||
<h2 className="text-xl font-bold text-slate-900 mt-1">
|
||
{showStoreOnboarding ? 'Add Store Outlet Location' : 'Store Directory'}
|
||
</h2>
|
||
</div>
|
||
|
||
<button
|
||
type="button"
|
||
onClick={() => setShowStoreOnboarding(!showStoreOnboarding)}
|
||
className="bg-[#662582] hover:bg-purple-800 text-white px-4 py-2.5 rounded-xl text-xs font-bold uppercase tracking-wider flex items-center gap-1.5 cursor-pointer shadow-sm active:scale-95 transition-all border-none"
|
||
>
|
||
{showStoreOnboarding ? 'View Store Directory' : '+ Add Store Branch'}
|
||
</button>
|
||
</div>
|
||
|
||
{showStoreOnboarding ? (
|
||
<div className="pt-2">
|
||
<AdminConsole activeTab="store" showHeader={false} onBack={() => setShowStoreOnboarding(false)} tenantId={tenantId} />
|
||
</div>
|
||
) : (
|
||
<>
|
||
{locationsQ.isLoading ? (
|
||
<div className="text-center py-lg text-slate-400 font-medium text-sm">Loading live outlets…</div>
|
||
) : cleanOutlets.length === 0 ? (
|
||
<div className="text-center py-lg text-slate-400 font-medium text-sm">No outlets configured yet.</div>
|
||
) : (
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-md max-h-[38rem] overflow-y-auto pr-1 scrollbar-thin">
|
||
{cleanOutlets.map((loc, i) => (
|
||
<div key={loc.locationid || i} className="p-5 border border-slate-200/60 rounded-2xl bg-white hover:border-purple-300 hover:shadow-md transition-all duration-350 flex flex-col justify-between group">
|
||
<div className="space-y-4">
|
||
{/* Header: Outlet name & status */}
|
||
<div className="flex justify-between items-start gap-3">
|
||
<div className="flex gap-3 flex-1 min-w-0">
|
||
<div className="p-3 rounded-xl bg-purple-50 text-purple-650 shrink-0 self-start group-hover:bg-purple-100 transition-colors">
|
||
<Store size={20} />
|
||
</div>
|
||
<div className="min-w-0">
|
||
<p className="font-bold text-slate-800 text-sm truncate leading-tight group-hover:text-purple-950 transition-colors">{loc.locationname}</p>
|
||
<p className="text-xs text-slate-450 mt-1.5 flex items-center gap-1">
|
||
<MapPin size={12} className="shrink-0 text-slate-400" />
|
||
<span className="truncate">{[loc.suburb, loc.city].filter(Boolean).join(', ') || '—'}</span>
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center justify-end gap-2 flex-wrap shrink-0">
|
||
<span className={`shrink-0 px-2.5 py-1 rounded-full text-[9px] font-black uppercase tracking-wider ${
|
||
loc.status.toLowerCase() === 'active'
|
||
? 'text-emerald-700 bg-emerald-50 border border-emerald-100/50'
|
||
: 'text-zinc-555 bg-zinc-100'
|
||
}`}>
|
||
{loc.status || '—'}
|
||
</span>
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
setEditingStore({ ...loc });
|
||
setShowEditStoreModal(true);
|
||
}}
|
||
className="text-[10px] uppercase font-bold text-purple-600 hover:text-purple-800 hover:bg-purple-50 px-2 py-1 rounded border border-purple-200 transition-colors"
|
||
>
|
||
Edit
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => handleDeleteStore(loc)}
|
||
className="text-[10px] uppercase font-bold text-red-600 hover:text-red-800 hover:bg-red-50 px-2 py-1 rounded border border-red-200 transition-colors"
|
||
>
|
||
Delete
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Outlet Details Grid */}
|
||
<div className="grid grid-cols-1 gap-3 bg-slate-50/50 p-3.5 rounded-xl border border-slate-100/80">
|
||
<div className="space-y-1">
|
||
<span className="text-[10px] text-slate-455 uppercase font-bold block">Delivery Range</span>
|
||
<p className="font-bold text-slate-700 text-xs">
|
||
{loc.deliveryradius ? `Up to ${loc.deliveryradius / 1000} km` : '—'}
|
||
</p>
|
||
</div>
|
||
<div className="space-y-1 border-t border-slate-100 pt-2 mt-1">
|
||
<span className="text-[10px] text-slate-455 uppercase font-bold block">Opening Hours</span>
|
||
<p className="font-bold text-slate-750 text-xs flex items-center gap-1.5 mt-0.5">
|
||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500" />
|
||
{loc.opentime && loc.closetime
|
||
? `Open: ${formatFriendlyTime(loc.opentime)} – ${formatFriendlyTime(loc.closetime)}`
|
||
: 'Hours not set'}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{activeTab === 'users' && (
|
||
<UsersPanel tenantId={tenantId} defaultNewUserRole={form.defaultNewUserRole} />
|
||
)}
|
||
|
||
|
||
|
||
</div>
|
||
</div>
|
||
|
||
{/* EDIT STORE MODAL */}
|
||
{showEditStoreModal && editingStore && (
|
||
<div
|
||
className="fixed inset-0 bg-[#0f172a]/40 backdrop-blur-sm z-[200] flex items-center justify-center p-4"
|
||
onClick={(e) => { if (e.target === e.currentTarget) setShowEditStoreModal(false); }}
|
||
>
|
||
<div className="bg-white border border-[#e2e8f0] rounded-xl w-[90vw] sm:w-[450px] max-h-[90vh] flex flex-col shadow-2xl overflow-hidden animate-in zoom-in-95 duration-200" onClick={(e) => e.stopPropagation()}>
|
||
<div className="p-4 border-b border-[#e2e8f0] bg-[#f8fafc] flex justify-between items-center shrink-0">
|
||
<h4 className="font-bold text-[#0f172a] text-sm flex items-center gap-2">
|
||
<Store size={16} className="text-[#662582]" />
|
||
Edit Store Outlet Location
|
||
</h4>
|
||
<button
|
||
type="button"
|
||
onClick={() => setShowEditStoreModal(false)}
|
||
className="text-zinc-400 hover:text-zinc-700 bg-zinc-100 hover:bg-zinc-200 p-1.5 rounded-lg transition-colors cursor-pointer border-none"
|
||
>
|
||
✕
|
||
</button>
|
||
</div>
|
||
|
||
<form onSubmit={handleUpdateStoreSubmit} className="flex flex-col flex-1 min-h-0">
|
||
<div className="p-4 overflow-y-auto space-y-4">
|
||
<div className="bg-purple-50/50 border border-purple-100 rounded-lg p-3 flex items-start gap-3">
|
||
<Store className="text-purple-600 mt-0.5 shrink-0" size={16} />
|
||
<div>
|
||
<h5 className="font-bold text-[#0f172a] text-xs">Update Outlet Profile</h5>
|
||
<p className="text-zinc-500 text-[11px] leading-relaxed mt-0.5">
|
||
Adjust the operational metadata for this node. The outlet will instantly be updated across the delivery fleet system.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 gap-4">
|
||
<div className="space-y-1">
|
||
<label className="font-bold text-zinc-500 uppercase tracking-widest text-[9px]">OUTLET NAME (*)</label>
|
||
<input
|
||
type="text"
|
||
placeholder="e.g. Coimbatore Main"
|
||
value={editingStore.locationname}
|
||
onChange={(e) => setEditingStore({ ...editingStore, locationname: 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-[#662582] text-xs"
|
||
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. RS Puram"
|
||
value={editingStore.suburb}
|
||
onChange={(e) => setEditingStore({ ...editingStore, suburb: 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-[#662582] text-xs"
|
||
required
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-1">
|
||
<label className="font-bold text-zinc-500 uppercase tracking-widest text-[9px]">CITY</label>
|
||
<input
|
||
type="text"
|
||
placeholder="e.g. Coimbatore"
|
||
value={editingStore.city}
|
||
onChange={(e) => setEditingStore({ ...editingStore, city: 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-[#662582] text-xs"
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-1">
|
||
<label className="font-bold text-zinc-500 uppercase tracking-widest text-[9px]">CONTACT PHONE (*)</label>
|
||
<input
|
||
type="text"
|
||
placeholder="e.g. 9876543210"
|
||
value={editingStore.contactno}
|
||
onChange={(e) => setEditingStore({ ...editingStore, 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-[#662582] text-xs"
|
||
required
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-1">
|
||
<label className="font-bold text-zinc-500 uppercase tracking-widest text-[9px]">STATUS</label>
|
||
<select
|
||
value={editingStore.status}
|
||
onChange={(e) => setEditingStore({ ...editingStore, status: 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-[#662582] text-xs"
|
||
>
|
||
<option value="Active">Active</option>
|
||
<option value="Inactive">Inactive</option>
|
||
<option value="Suspended">Suspended</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="p-4 border-t border-[#f1f5f9] flex justify-end gap-3 bg-[#f8fafc] shrink-0">
|
||
<button
|
||
type="button"
|
||
onClick={() => setShowEditStoreModal(false)}
|
||
className="px-4 py-2 border border-[#e2e8f0] rounded-lg font-semibold text-zinc-500 hover:bg-zinc-50 cursor-pointer text-xs"
|
||
>
|
||
Cancel
|
||
</button>
|
||
<button
|
||
type="submit"
|
||
disabled={updateLocationMut.isPending}
|
||
className="px-4 py-2 bg-[#662582] hover:bg-purple-800 text-white rounded-lg font-bold shadow-sm cursor-pointer border-none flex items-center gap-2 text-xs"
|
||
>
|
||
{updateLocationMut.isPending ? 'Updating...' : 'Update Outlet Profile'}
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<ConfirmModal
|
||
isOpen={confirmModalState.isOpen}
|
||
title="Delete Outlet"
|
||
message={
|
||
<>
|
||
Are you sure you want to delete <strong>{confirmModalState.loc?.locationname}</strong>? This action cannot be undone.
|
||
</>
|
||
}
|
||
confirmText="Delete Outlet"
|
||
onConfirm={confirmDeleteOutlet}
|
||
onCancel={() => setConfirmModalState({ isOpen: false, loc: null })}
|
||
isDestructive={true}
|
||
isLoading={deleteLocationMut.isPending}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|