stock request
This commit is contained in:
@@ -42,6 +42,7 @@ import {
|
||||
useFiestaProductCategories,
|
||||
useFiestaMasterCatalog,
|
||||
useFiestaUpdateStockRequest,
|
||||
useFiestaGetStockRequests,
|
||||
} from '../services/fiestaQueries';
|
||||
import { FIESTA_TENANT_ID, str as fstr } from '../services/fiestaApi';
|
||||
import { stockRowToProduct, stockRowToInventory } from '../services/fiestaMappers';
|
||||
@@ -51,7 +52,6 @@ import AwaitingApi from './AwaitingApi';
|
||||
import { SlideDrawer, Skeleton, TH_STYLE, SURFACE_ALT, TEXT, TEXT_2, TEXT_3, BORDER, BRAND, tint, edge, StatusChip } from './consoleUi';
|
||||
import FMCGHoverOverlay from './FMCGHoverOverlay';
|
||||
import { useCompare } from '../contexts/CompareContext';
|
||||
import TrialBatchDrawer, { TrialProduct } from './TrialBatchDrawer';
|
||||
|
||||
const MOCK_GLOBAL_CATALOG: ProductMatrixItem[] = [
|
||||
{
|
||||
@@ -122,7 +122,7 @@ export default function InventoryView({
|
||||
tenantId = FIESTA_TENANT_ID
|
||||
}: InventoryViewProps) {
|
||||
const { selectedProducts, toggleProduct, setIsComparing, clearSelection, setHideCompareBar } = useCompare();
|
||||
const [trialProducts, setTrialProducts] = useState<TrialProduct[]>([]);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
// ── Live stock across every outlet (Fiesta) ───────────────────────────────
|
||||
// This page is the admin's command surface. The GLOBAL CATALOG is the deduped
|
||||
@@ -185,99 +185,70 @@ export default function InventoryView({
|
||||
const [requestStoreFilter, setRequestStoreFilter] = useState('All Stores');
|
||||
|
||||
const [storeRequests, setStoreRequests] = useState<{ locationid: number, locationname: string, picks: Record<string, { qty: number; status: 'Pending' | 'Approved' | 'Rejected' | 'Cancelled'; requestedAt: string; resolvedAt?: string }> }[]>([]);
|
||||
const stockRequestsQ = useFiestaGetStockRequests({ tenantid: tenantId, pagesize: 1000 });
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab === 'requests') {
|
||||
const reqs: any[] = [];
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
if (key && key.startsWith('nearledaily.catalogue.request.')) {
|
||||
const locIdStr = key.split('.').pop();
|
||||
if (locIdStr && locIdStr !== 'na') {
|
||||
const locId = parseInt(locIdStr);
|
||||
const raw = localStorage.getItem(key);
|
||||
if (raw) {
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
const picks: Record<string, any> = {};
|
||||
for (const [k, v] of Object.entries(parsed)) {
|
||||
if (typeof v === 'number') picks[k] = { qty: v, status: 'Pending', requestedAt: new Date().toISOString() };
|
||||
else {
|
||||
picks[k] = v;
|
||||
if (picks[k].status === 'Approve') picks[k].status = 'Approved';
|
||||
if (picks[k].status === 'Reject') picks[k].status = 'Rejected';
|
||||
}
|
||||
}
|
||||
if (Object.keys(picks).length > 0) {
|
||||
const loc = locations.find(l => l.locationid === locId);
|
||||
reqs.push({
|
||||
locationid: locId,
|
||||
locationname: loc ? loc.locationname : `Outlet #${locId}`,
|
||||
picks
|
||||
});
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
if (activeTab === 'requests' && stockRequestsQ.data) {
|
||||
const reqsMap: Record<number, any> = {};
|
||||
stockRequestsQ.data.forEach((req: any) => {
|
||||
if (!reqsMap[req.locationid]) {
|
||||
const loc = locations.find(l => l.locationid === req.locationid);
|
||||
reqsMap[req.locationid] = {
|
||||
locationid: req.locationid,
|
||||
locationname: loc ? loc.locationname : `Outlet #${req.locationid}`,
|
||||
picks: {}
|
||||
};
|
||||
}
|
||||
}
|
||||
setStoreRequests(reqs);
|
||||
if (!reqsMap[req.locationid].picks[req.productid]) {
|
||||
reqsMap[req.locationid].picks[req.productid] = {
|
||||
qty: req.qty,
|
||||
status: req.status,
|
||||
requestedAt: req.created,
|
||||
resolvedAt: req.updated,
|
||||
productname: req.productname,
|
||||
productimage: req.productimage,
|
||||
requestid: req.requestid
|
||||
};
|
||||
}
|
||||
});
|
||||
setStoreRequests(Object.values(reqsMap).filter(r => Object.keys(r.picks).length > 0));
|
||||
}
|
||||
}, [activeTab, locations]);
|
||||
}, [activeTab, locations, stockRequestsQ.data]);
|
||||
|
||||
const updateStockRequestMutation = useFiestaUpdateStockRequest();
|
||||
|
||||
const updateProductRequestStatus = (locationid: number, productid: string, status: 'Approved' | 'Rejected' | 'Pending') => {
|
||||
// Optimistic API update
|
||||
// Find the requestid from the current state
|
||||
let requestid = 0;
|
||||
storeRequests.forEach(r => {
|
||||
if (r.locationid === locationid && r.picks[productid]) {
|
||||
requestid = r.picks[productid].requestid;
|
||||
}
|
||||
});
|
||||
|
||||
if (!requestid) return;
|
||||
|
||||
updateStockRequestMutation.mutate({
|
||||
tenantid: tenantId,
|
||||
locationid,
|
||||
productid: Number(productid),
|
||||
requestid,
|
||||
status
|
||||
});
|
||||
|
||||
const key = `nearledaily.catalogue.request.${locationid}`;
|
||||
const raw = localStorage.getItem(key);
|
||||
if (raw) {
|
||||
try {
|
||||
const picks = JSON.parse(raw);
|
||||
if (picks[productid] != null) {
|
||||
const existing = typeof picks[productid] === 'number'
|
||||
? { qty: picks[productid], status: 'Pending', requestedAt: new Date().toISOString() }
|
||||
: picks[productid];
|
||||
|
||||
picks[productid] = { ...existing, status, resolvedAt: status === 'Pending' ? undefined : new Date().toISOString() };
|
||||
localStorage.setItem(key, JSON.stringify(picks));
|
||||
|
||||
if (status === 'Approved') {
|
||||
const prod = products.find(p => String(p.id) === String(productid)) || MOCK_GLOBAL_CATALOG.find(p => String(p.id) === String(productid));
|
||||
if (prod) {
|
||||
if (storeCat.has(productid)) {
|
||||
storeCat.setQty(productid, storeCat.getQty(productid) + existing.qty);
|
||||
} else {
|
||||
storeCat.add({
|
||||
productid: String(prod.id),
|
||||
name: prod.name,
|
||||
image: prod.image,
|
||||
category: prod.category,
|
||||
sku: prod.sku,
|
||||
price: prod.unitsSold > 0 ? Math.round(prod.revenue / prod.unitsSold) : 10,
|
||||
unit: prod.exposure || 'Piece',
|
||||
qty: existing.qty
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setStoreRequests(prev => prev.map(r => {
|
||||
if (r.locationid === locationid) {
|
||||
return { ...r, picks: { ...r.picks, [productid]: picks[productid] } };
|
||||
}
|
||||
return r;
|
||||
}));
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
// Optimistic UI update
|
||||
setStoreRequests(prev => prev.map(r => {
|
||||
if (r.locationid === locationid && r.picks[productid]) {
|
||||
return {
|
||||
...r,
|
||||
picks: {
|
||||
...r.picks,
|
||||
[productid]: { ...r.picks[productid], status, resolvedAt: new Date().toISOString() }
|
||||
}
|
||||
};
|
||||
}
|
||||
return r;
|
||||
}));
|
||||
};
|
||||
|
||||
// Hide compare bar when not in Global Catalogue
|
||||
@@ -1020,22 +991,6 @@ export default function InventoryView({
|
||||
<Plus size={14} /> Import to Catalogue
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setTrialProducts([{
|
||||
id: prod.id,
|
||||
name: prod.name,
|
||||
sku: prod.sku,
|
||||
category: prod.category.split(' / ')[0],
|
||||
price: prod.revenue / Math.max(1, prod.unitsSold),
|
||||
image: prod.image
|
||||
}]);
|
||||
}}
|
||||
className="w-full flex items-center justify-center gap-1.5 py-2 rounded-xl text-[11px] font-bold transition-all bg-indigo-50 text-indigo-700 hover:bg-indigo-600 hover:text-white cursor-pointer border border-indigo-100 hover:border-indigo-600"
|
||||
>
|
||||
<Package size={14} /> Request Sample
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1185,22 +1140,6 @@ export default function InventoryView({
|
||||
<Plus size={14} /> Import to Catalogue
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setTrialProducts([{
|
||||
id: prod.id,
|
||||
name: prod.name,
|
||||
sku: prod.sku,
|
||||
category: prod.category.split(' / ')[0],
|
||||
price: prod.revenue / Math.max(1, prod.unitsSold),
|
||||
image: prod.image
|
||||
}]);
|
||||
}}
|
||||
className="w-full flex items-center justify-center gap-1.5 py-2 rounded-xl text-[11px] font-bold transition-all bg-indigo-50 text-indigo-700 hover:bg-indigo-600 hover:text-white cursor-pointer border border-indigo-100 hover:border-indigo-600"
|
||||
>
|
||||
<Package size={14} /> Request Sample
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1263,7 +1202,7 @@ export default function InventoryView({
|
||||
<Inbox size={32} className="text-slate-400" />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-slate-700 tracking-tight">No Pending Requests</h3>
|
||||
<p className="text-sm font-medium text-slate-400 mt-2 text-center max-w-sm leading-relaxed">
|
||||
<p className="text-sm font-medium text-slate-400 mt-2 text-center w-full max-w-[400px] leading-relaxed">
|
||||
You're all caught up! All store stock requests have been processed and there is currently no pending action required.
|
||||
</p>
|
||||
</div>
|
||||
@@ -1505,22 +1444,6 @@ export default function InventoryView({
|
||||
>
|
||||
<Plus size={18} strokeWidth={2} /> Import Local
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setTrialProducts([{
|
||||
id: selectedAdminProduct.id,
|
||||
name: selectedAdminProduct.name,
|
||||
sku: selectedAdminProduct.sku,
|
||||
category: String(selectedAdminProduct.category || '').split(' / ')[0],
|
||||
price: selectedAdminProduct.revenue / Math.max(1, selectedAdminProduct.unitsSold),
|
||||
image: selectedAdminProduct.image
|
||||
}]);
|
||||
}}
|
||||
className="w-auto px-6 flex items-center justify-center gap-2 py-3.5 rounded-xl text-sm font-semibold transition-colors bg-white text-slate-700 border border-slate-200 hover:bg-slate-50"
|
||||
title="Request Sample"
|
||||
>
|
||||
<Package size={18} strokeWidth={2} /> Sample
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1661,23 +1584,6 @@ export default function InventoryView({
|
||||
})()}
|
||||
</SlideDrawer>
|
||||
|
||||
<TrialBatchDrawer
|
||||
isOpen={trialProducts.length > 0}
|
||||
onClose={() => setTrialProducts([])}
|
||||
product={trialProducts[0] || null}
|
||||
tenantId={tenantId}
|
||||
onSuccess={(tp) => {
|
||||
const globalProd = MOCK_GLOBAL_CATALOG.find(p => p.id === tp.id);
|
||||
if (globalProd) {
|
||||
setProducts(prev => {
|
||||
if (prev.some(p => p.id === globalProd.id)) return prev;
|
||||
return [{ ...globalProd, isSample: true, isNew: true }, ...prev];
|
||||
});
|
||||
setActiveTab('catalog');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* ── Floating Import Button ── */}
|
||||
{activeTab === 'import_branding' && selectedProducts.length > 0 && (
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user