stock request
This commit is contained in:
@@ -17,12 +17,13 @@ import {
|
||||
Plus,
|
||||
Bike
|
||||
} from 'lucide-react';
|
||||
import { useFiestaAllTenants, useFiestaTenantLocations } from '../services/fiestaQueries';
|
||||
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';
|
||||
@@ -143,6 +144,50 @@ export default function SettingsView({ tenantId = FIESTA_TENANT_ID, user }: Sett
|
||||
// (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(() => {
|
||||
@@ -210,11 +255,10 @@ export default function SettingsView({ tenantId = FIESTA_TENANT_ID, user }: Sett
|
||||
}, [rolesQ.data]);
|
||||
|
||||
return (
|
||||
<div className="space-y-lg animate-in fade-in duration-300 relative font-sans text-slate-700">
|
||||
<div className="space-y-3 font-sans text-slate-700">
|
||||
{/* Header */}
|
||||
<div className="pb-4 border-b border-slate-100">
|
||||
|
||||
<div className="mt-3">
|
||||
<div>
|
||||
<div>
|
||||
{tenantsQ.isLoading ? (
|
||||
<span className="inline-flex items-center gap-1.5 text-xs font-bold text-slate-400 uppercase tracking-wider">
|
||||
<span className="w-2 h-2 rounded-full bg-slate-350 animate-pulse" /> Loading store profile…
|
||||
@@ -420,7 +464,7 @@ export default function SettingsView({ tenantId = FIESTA_TENANT_ID, user }: Sett
|
||||
<div className="space-y-4">
|
||||
{/* Header: Outlet name & status */}
|
||||
<div className="flex justify-between items-start gap-3">
|
||||
<div className="flex 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>
|
||||
@@ -432,30 +476,43 @@ export default function SettingsView({ tenantId = FIESTA_TENANT_ID, user }: Sett
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
<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-2 gap-3 bg-slate-50/50 p-3.5 rounded-xl border border-slate-100/80">
|
||||
<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">
|
||||
<span className="text-[10px] text-slate-455 uppercase font-bold block">Delivery Speed</span>
|
||||
<p className="font-bold text-slate-700 text-xs">
|
||||
{loc.deliverymins ? `${loc.deliverymins} mins avg` : '—'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-1 col-span-2 border-t border-slate-100 pt-2 mt-1">
|
||||
<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" />
|
||||
@@ -483,6 +540,138 @@ export default function SettingsView({ tenantId = FIESTA_TENANT_ID, user }: Sett
|
||||
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user