stock request

This commit is contained in:
2026-07-04 17:46:30 +05:30
parent c6a3ea51da
commit 7cd0ae85ef
23 changed files with 1293 additions and 570 deletions

View File

@@ -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