inventory page

This commit is contained in:
2026-07-23 16:05:38 +05:30
parent c6649c9c8f
commit 92165b986c
9 changed files with 150 additions and 71 deletions

View File

@@ -115,7 +115,7 @@ export default function App() {
const [isCoimbatoreView, setIsCoimbatoreView] = useState(false); const [isCoimbatoreView, setIsCoimbatoreView] = useState(false);
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [sidebarOpen, setSidebarOpen] = useState(() => typeof window !== 'undefined' ? window.innerWidth >= 768 : true); const [sidebarOpen, setSidebarOpen] = useState(false);
// Auto adapt sidebar state on window resize // Auto adapt sidebar state on window resize
useEffect(() => { useEffect(() => {

View File

@@ -24,7 +24,7 @@ export default function CatalogueBrowser({ tenantid, locationid, onClose }: Cata
const [importingProduct, setImportingProduct] = useState<CatalogueProduct | null>(null); const [importingProduct, setImportingProduct] = useState<CatalogueProduct | null>(null);
const [isLocalSidebarOpen, setIsLocalSidebarOpen] = useState(true); const [isLocalSidebarOpen, setIsLocalSidebarOpen] = useState(false);
// Debounce search // Debounce search
React.useEffect(() => { React.useEffect(() => {
@@ -42,7 +42,7 @@ export default function CatalogueBrowser({ tenantid, locationid, onClose }: Cata
const { data: importedRefs = new Set<string>() } = useImportedCatalogueRefs(tenantid, brand); const { data: importedRefs = new Set<string>() } = useImportedCatalogueRefs(tenantid, brand);
const { data: tenantCategories = [] } = useTenantCategories(tenantid); const { data: tenantCategories = [] } = useTenantCategories(tenantid);
const storeCat = useStoreCatalogue(); const storeCat = useStoreCatalogue(tenantid, locationid);
const importProductMutation = useImportCatalogueProduct(tenantid, locationid); const importProductMutation = useImportCatalogueProduct(tenantid, locationid);
@@ -238,7 +238,10 @@ export default function CatalogueBrowser({ tenantid, locationid, onClose }: Cata
subcategoryid: 0, subcategoryid: 0,
quantity: 1, quantity: 1,
stocktype: "in", stocktype: "in",
status: "Active", // Draft: lands in the Admin Catalogue only. It only becomes
// visible to store users once the admin explicitly publishes
// it via "Add to Store Catalogue" (InventoryView).
status: "Draft",
retailprice: 0, retailprice: 0,
productcost: 0, productcost: 0,
taxpercent: 0, taxpercent: 0,

View File

@@ -54,7 +54,9 @@ export default function ImportProductModal({
subcategoryid: subcategoryId ? Number(subcategoryId) : 0, subcategoryid: subcategoryId ? Number(subcategoryId) : 0,
quantity: Number(quantity), quantity: Number(quantity),
stocktype: "in", stocktype: "in",
status: "Active", // Draft: lands in the Admin Catalogue only, published to the store
// catalogue separately via the explicit "Add to Store Catalogue" step.
status: "Draft",
retailprice: Number(retailPrice), retailprice: Number(retailPrice),
productcost: Number(productCost), productcost: Number(productCost),
taxpercent: Number(taxPercent), taxpercent: Number(taxPercent),
@@ -143,7 +145,7 @@ export default function ImportProductModal({
subcategoryid: 0, subcategoryid: 0,
quantity: 1, quantity: 1,
stocktype: "in", stocktype: "in",
status: "Active", status: "Draft",
retailprice: 0, retailprice: 0,
productcost: 0, productcost: 0,
taxpercent: 0, taxpercent: 0,

View File

@@ -97,6 +97,11 @@ export default function InventoryView({
[locationsQ.data], [locationsQ.data],
); );
// The admin catalogue (imports, curation) is scoped to this tenant's own
// primary/hub outlet — not the hardcoded Fiesta defaults, so onboarded
// tenants other than Fiesta see their own imports reflected here.
const primaryLocationId = locations[0]?.locationid ?? FIESTA_PRIMARY_LOCATION_ID;
const storesStock = useFiestaStoresStock( const storesStock = useFiestaStoresStock(
tenantId, tenantId,
locations.map(({ locationid, locationname }) => ({ locationid, locationname })), locations.map(({ locationid, locationname }) => ({ locationid, locationname })),
@@ -113,11 +118,11 @@ export default function InventoryView({
const allStoreRows = storesStock.flatMap((s) => s.rows); const allStoreRows = storesStock.flatMap((s) => s.rows);
const [activeTab, setActiveTab] = useState<'catalog' | 'requests' | 'global_catalogue'>('catalog'); const [activeTab, setActiveTab] = useState<'catalog' | 'requests' | 'global_catalogue'>('catalog');
const [isLocalSidebarOpen, setIsLocalSidebarOpen] = useState(true); const [isLocalSidebarOpen, setIsLocalSidebarOpen] = useState(false);
const [selectedCategories, setSelectedCategories] = useState<string[]>([]); const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
const [hoveredAdminProduct, setHoveredAdminProduct] = useState<ProductMatrixItem | null>(null); const [hoveredAdminProduct, setHoveredAdminProduct] = useState<ProductMatrixItem | null>(null);
const [localSearch, setLocalSearch] = useState(''); const [localSearch, setLocalSearch] = useState('');
const storeCat = useStoreCatalogue(); const storeCat = useStoreCatalogue(tenantId, primaryLocationId);
const [importPrice, setImportPrice] = useState<string>(''); const [importPrice, setImportPrice] = useState<string>('');
const [isSettingPrice, setIsSettingPrice] = useState(false); const [isSettingPrice, setIsSettingPrice] = useState(false);
const [addingPriceProdId, setAddingPriceProdId] = useState<string | null>(null); const [addingPriceProdId, setAddingPriceProdId] = useState<string | null>(null);
@@ -613,7 +618,10 @@ export default function InventoryView({
disabled={!cardImportPrice || Number(cardImportPrice) <= 0} disabled={!cardImportPrice || Number(cardImportPrice) <= 0}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
storeCat.add({ productid: prod.id, name: prod.name, image: prod.image, category: prod.category, sku: prod.sku, price: Number(cardImportPrice), unit: prod.exposure, qty: 1, status: 'Active' }); // qty: 0 — publishing to the catalogue is a listing/pricing action,
// not a stock delivery. Real quantity only lands via the
// request → approve → Mark as Received flow.
storeCat.add({ productid: prod.id, name: prod.name, image: prod.image, category: prod.category, sku: prod.sku, price: Number(cardImportPrice), unit: prod.exposure, qty: 0, status: 'Active' });
setAddingPriceProdId(null); setAddingPriceProdId(null);
}} }}
className="flex-1 py-1 bg-[#662582] text-white text-[9px] font-bold disabled:opacity-50" className="flex-1 py-1 bg-[#662582] text-white text-[9px] font-bold disabled:opacity-50"
@@ -746,6 +754,7 @@ export default function InventoryView({
const isRejected = req.pickData.status === 'Rejected'; const isRejected = req.pickData.status === 'Rejected';
const isPending = req.pickData.status === 'Pending'; const isPending = req.pickData.status === 'Pending';
const isCancelled = req.pickData.status === 'Cancelled'; const isCancelled = req.pickData.status === 'Cancelled';
const isReceived = req.pickData.status === 'Received';
return ( return (
<tr <tr
@@ -787,15 +796,17 @@ export default function InventoryView({
</td> </td>
<td className="px-5 py-4 whitespace-nowrap"> <td className="px-5 py-4 whitespace-nowrap">
<span className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-bold border ${ <span className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-bold border ${
isApproved ? 'bg-emerald-50 text-emerald-700 border-emerald-200' : isReceived ? 'bg-indigo-50 text-indigo-700 border-indigo-200' :
isRejected ? 'bg-rose-50 text-rose-700 border-rose-200' : isApproved ? 'bg-emerald-50 text-emerald-700 border-emerald-200' :
isCancelled ? 'bg-slate-50 text-slate-700 border-slate-200' : isRejected ? 'bg-rose-50 text-rose-700 border-rose-200' :
isCancelled ? 'bg-slate-50 text-slate-700 border-slate-200' :
'bg-amber-50 text-amber-700 border-amber-200' 'bg-amber-50 text-amber-700 border-amber-200'
}`}> }`}>
<span className={`w-1.5 h-1.5 rounded-full ${ <span className={`w-1.5 h-1.5 rounded-full ${
isApproved ? 'bg-emerald-500' : isReceived ? 'bg-indigo-500' :
isRejected ? 'bg-rose-500' : isApproved ? 'bg-emerald-500' :
isCancelled ? 'bg-slate-400' : isRejected ? 'bg-rose-500' :
isCancelled ? 'bg-slate-400' :
'bg-amber-500 animate-pulse' 'bg-amber-500 animate-pulse'
}`} /> }`} />
{req.pickData.status || '—'} {req.pickData.status || '—'}
@@ -843,7 +854,7 @@ export default function InventoryView({
<div className="flex-1 flex flex-col min-h-0 overflow-hidden animate-in fade-in duration-300"> <div className="flex-1 flex flex-col min-h-0 overflow-hidden animate-in fade-in duration-300">
<CatalogueBrowser <CatalogueBrowser
tenantid={tenantId} tenantid={tenantId}
locationid={FIESTA_PRIMARY_LOCATION_ID} locationid={primaryLocationId}
onClose={() => setActiveTab('catalog')} onClose={() => setActiveTab('catalog')}
/> />
</div> </div>
@@ -940,19 +951,24 @@ export default function InventoryView({
</div> </div>
</div> </div>
<button <button
disabled={!importPrice || Number(importPrice) <= 0} disabled={!importPrice || Number(importPrice) <= 0}
onClick={() => { onClick={() => {
storeCat.add({ // First-time publish gets qty 0 — it's a listing/pricing action, not a
productid: selectedAdminProduct.id, // stock delivery; real quantity only lands via Mark as Received. If this
name: selectedAdminProduct.name, // product is already published, preserve its existing qty so a mere price
image: selectedAdminProduct.image, // edit here doesn't zero out stock that's since been received.
category: selectedAdminProduct.category, const existingQty = storeCat.items.find(i => i.productid === selectedAdminProduct.id)?.qty ?? 0;
sku: selectedAdminProduct.sku, storeCat.add({
price: Number(importPrice), productid: selectedAdminProduct.id,
unit: selectedAdminProduct.exposure || 'Pc', name: selectedAdminProduct.name,
qty: 1, image: selectedAdminProduct.image,
status: 'Active' category: selectedAdminProduct.category,
sku: selectedAdminProduct.sku,
price: Number(importPrice),
unit: selectedAdminProduct.exposure || 'Pc',
qty: existingQty,
status: 'Active'
}); });
}} }}
className="w-full flex items-center justify-center gap-2 py-3 rounded-xl text-xs font-extrabold transition-all bg-[#662582] hover:bg-[#531e6a] text-white shadow-md disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer" className="w-full flex items-center justify-center gap-2 py-3 rounded-xl text-xs font-extrabold transition-all bg-[#662582] hover:bg-[#531e6a] text-white shadow-md disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer"

View File

@@ -21,7 +21,7 @@
import React, { useEffect, useMemo, useState } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { Search, Boxes, Layers, Plus, Minus, Check, CheckCircle2, X, Store, PackageSearch, Activity, Info, Inbox } from 'lucide-react'; import { Search, Boxes, Layers, Plus, Minus, Check, CheckCircle2, X, Store, PackageSearch, Activity, Info, Inbox } from 'lucide-react';
import { useFiestaStockStatement, useFiestaCreateStockRequest, useFiestaGetStockRequests, useFiestaUpdateStockRequest, FIESTA_TENANT_ID } from '../services/fiestaQueries'; import { useFiestaStockStatement, useFiestaCreateStockRequest, useFiestaGetStockRequests, useFiestaUpdateStockRequest, useFiestaCreateProductLocation, FIESTA_TENANT_ID } from '../services/fiestaQueries';
import { num as fnum, str as fstr, type Row, FIESTA_PRIMARY_LOCATION_ID } from '../services/fiestaApi'; import { num as fnum, str as fstr, type Row, FIESTA_PRIMARY_LOCATION_ID } from '../services/fiestaApi';
import { useStoreCatalogue } from '../services/storeCatalogue'; import { useStoreCatalogue } from '../services/storeCatalogue';
import AwaitingApi from './AwaitingApi'; import AwaitingApi from './AwaitingApi';
@@ -56,7 +56,7 @@ function catBadgeClass(category: string): string {
export default function StoreCatalogView({ locationid, storeName = 'your store', tenantId = FIESTA_TENANT_ID, isSidebarOpen = false }: StoreCatalogViewProps) { export default function StoreCatalogView({ locationid, storeName = 'your store', tenantId = FIESTA_TENANT_ID, isSidebarOpen = false }: StoreCatalogViewProps) {
const tenantid = tenantId; const tenantid = tenantId;
const [view, setView] = useState<'catalogue' | 'inventory' | 'requests'>('catalogue'); const [view, setView] = useState<'catalogue' | 'inventory' | 'requests'>('catalogue');
const [isLocalSidebarOpen, setIsLocalSidebarOpen] = useState(true); const [isLocalSidebarOpen, setIsLocalSidebarOpen] = useState(false);
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [selectedCategories, setSelectedCategories] = useState<string[]>([]); const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
const [requestDate, setRequestDate] = useState(() => new Date().toISOString().split('T')[0]); const [requestDate, setRequestDate] = useState(() => new Date().toISOString().split('T')[0]);
@@ -68,7 +68,7 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
const [notice, setNotice] = useState(false); const [notice, setNotice] = useState(false);
// The admin-curated catalogue (what the user is allowed to pick from). // The admin-curated catalogue (what the user is allowed to pick from).
const storeCat = useStoreCatalogue(); const storeCat = useStoreCatalogue(tenantid, locationid ?? FIESTA_PRIMARY_LOCATION_ID);
const products = useMemo( const products = useMemo(
() => () =>
storeCat.items storeCat.items
@@ -109,6 +109,43 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
const createRequestMutation = useFiestaCreateStockRequest(); const createRequestMutation = useFiestaCreateStockRequest();
const updateRequestMutation = useFiestaUpdateStockRequest(); const updateRequestMutation = useFiestaUpdateStockRequest();
const stockInMutation = useFiestaCreateProductLocation();
// User confirms an approved request actually arrived — this is the only
// point a request turns into real, live stock. It first writes the stock-in
// movement, then marks the request "Received" once that succeeds.
//
// createproductlocation sets the product's quantity rather than incrementing
// it (same assumption the publish-to-catalogue flow relies on), so this must
// send the new total on hand, not just the newly-received amount, or a
// receive here would wipe out whatever was already in stock.
const handleMarkReceived = (data: any) => {
const existingQty = storeCat.items.find(i => i.productid === String(data.productid))?.qty ?? 0;
stockInMutation.mutate(
{
tenantid,
locationid: locationid ?? FIESTA_PRIMARY_LOCATION_ID,
productid: Number(data.productid),
quantity: existingQty + (Number(data.qty) || 0),
stocktype: 'in',
status: 'Active',
},
{
onSuccess: () => {
updateRequestMutation.mutate({
tenantid,
locationid: locationid ?? FIESTA_PRIMARY_LOCATION_ID,
productid: Number(data.productid),
requestid: data.requestid,
status: 'Received',
});
},
onError: (err: any) => {
alert(err.message || 'Failed to record received stock.');
},
},
);
};
const togglePick = (id: string) => { const togglePick = (id: string) => {
setNotice(false); setNotice(false);
@@ -158,8 +195,10 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
const inStore = useMemo(() => { const inStore = useMemo(() => {
const set = new Set((stockQ.data ?? []).map((r) => fstr(r.productid))); const set = new Set((stockQ.data ?? []).map((r) => fstr(r.productid)));
// Only "Received" requests represent real, confirmed stock — "Approved"
// just means the admin signed off, not that it has physically arrived.
Object.entries(picks).forEach(([pid, data]: [string, any]) => { Object.entries(picks).forEach(([pid, data]: [string, any]) => {
if (data.status === 'Approved') set.add(pid); if (data.status === 'Received') set.add(pid);
}); });
return set; return set;
}, [stockQ.data, picks]); }, [stockQ.data, picks]);
@@ -181,11 +220,12 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
}; };
}); });
// Merge approved picks into the inventory // Merge received picks into the inventory — stock only actually lands
// once the user confirms receipt, not merely once the admin approves.
const inventoryMap = new Map(baseInventory.map(item => [item.id, item])); const inventoryMap = new Map(baseInventory.map(item => [item.id, item]));
Object.entries(picks).forEach(([pid, data]: [string, any]) => { Object.entries(picks).forEach(([pid, data]: [string, any]) => {
if (data.status === 'Approved') { if (data.status === 'Received') {
if (inventoryMap.has(pid)) { if (inventoryMap.has(pid)) {
const item = inventoryMap.get(pid)!; const item = inventoryMap.get(pid)!;
item.qty += data.qty; item.qty += data.qty;
@@ -439,7 +479,7 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
{filtered.map((p) => { {filtered.map((p) => {
const stocked = inStore.has(p.id); const stocked = inStore.has(p.id);
const pick = picks[p.id]; const pick = picks[p.id];
const picked = pick != null && pick.status !== 'Cancelled' && pick.status !== 'Approved'; const picked = pick != null && pick.status !== 'Cancelled' && pick.status !== 'Received';
return ( return (
<div key={p.id} onClick={() => setSelectedProduct(p)} className="bg-white/80 backdrop-blur-md border border-[#e2e8f0] rounded-none flex flex-col shadow-sm hover:shadow-[0_12px_24px_rgba(99,102,241,0.06)] hover:border-[#662582]/40 hover:-translate-y-1 transition-all duration-300 relative group overflow-hidden cursor-pointer"> <div key={p.id} onClick={() => setSelectedProduct(p)} className="bg-white/80 backdrop-blur-md border border-[#e2e8f0] rounded-none flex flex-col shadow-sm hover:shadow-[0_12px_24px_rgba(99,102,241,0.06)] hover:border-[#662582]/40 hover:-translate-y-1 transition-all duration-300 relative group overflow-hidden cursor-pointer">
{/* Image Section - Top */} {/* Image Section - Top */}
@@ -481,11 +521,12 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
{picks[p.id].status === 'Pending' && ( {picks[p.id].status === 'Pending' && (
<button onClick={(e) => { e.stopPropagation(); setTempQty(picks[p.id].qty); setActiveQtyProduct(p); }} title="Edit Quantity" className="px-2 py-0.5 text-amber-600 hover:bg-amber-100 bg-white border border-amber-200 transition-colors shadow-sm text-[9px] font-bold">Edit</button> <button onClick={(e) => { e.stopPropagation(); setTempQty(picks[p.id].qty); setActiveQtyProduct(p); }} title="Edit Quantity" className="px-2 py-0.5 text-amber-600 hover:bg-amber-100 bg-white border border-amber-200 transition-colors shadow-sm text-[9px] font-bold">Edit</button>
)} )}
<button onClick={(e) => { e.stopPropagation(); togglePick(p.id); }} title="Remove" className={`px-1.5 py-0.5 bg-white border transition-colors shadow-sm ${ {picks[p.id].status !== 'Approved' && (
picks[p.id].status === 'Approved' ? 'text-emerald-500 border-emerald-200' : <button onClick={(e) => { e.stopPropagation(); togglePick(p.id); }} title="Remove" className={`px-1.5 py-0.5 bg-white border transition-colors shadow-sm ${
picks[p.id].status === 'Rejected' ? 'text-rose-500 border-rose-200' : picks[p.id].status === 'Rejected' ? 'text-rose-500 border-rose-200' :
'text-amber-500 border-amber-200' 'text-amber-500 border-amber-200'
}`}><X size={12} /></button> }`}><X size={12} /></button>
)}
</div> </div>
</div> </div>
) : ( ) : (
@@ -532,6 +573,7 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
const isApproved = data.status === 'Approved'; const isApproved = data.status === 'Approved';
const isRejected = data.status === 'Rejected'; const isRejected = data.status === 'Rejected';
const isCancelled = data.status === 'Cancelled'; const isCancelled = data.status === 'Cancelled';
const isReceived = data.status === 'Received';
return ( return (
<tr key={data.requestid} className="transition-all duration-200 hover:bg-slate-50/80 group"> <tr key={data.requestid} className="transition-all duration-200 hover:bg-slate-50/80 group">
@@ -571,15 +613,17 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
</td> </td>
<td className="px-5 py-4 whitespace-nowrap"> <td className="px-5 py-4 whitespace-nowrap">
<span className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-bold border ${ <span className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-bold border ${
isApproved ? 'bg-emerald-50 text-emerald-700 border-emerald-200' : isReceived ? 'bg-indigo-50 text-indigo-700 border-indigo-200' :
isRejected ? 'bg-rose-50 text-rose-700 border-rose-200' : isApproved ? 'bg-emerald-50 text-emerald-700 border-emerald-200' :
isCancelled ? 'bg-slate-50 text-slate-700 border-slate-200' : isRejected ? 'bg-rose-50 text-rose-700 border-rose-200' :
isCancelled ? 'bg-slate-50 text-slate-700 border-slate-200' :
'bg-amber-50 text-amber-700 border-amber-200' 'bg-amber-50 text-amber-700 border-amber-200'
}`}> }`}>
<span className={`w-1.5 h-1.5 rounded-full ${ <span className={`w-1.5 h-1.5 rounded-full ${
isApproved ? 'bg-emerald-500' : isReceived ? 'bg-indigo-500' :
isRejected ? 'bg-rose-500' : isApproved ? 'bg-emerald-500' :
isCancelled ? 'bg-slate-400' : isRejected ? 'bg-rose-500' :
isCancelled ? 'bg-slate-400' :
'bg-amber-500 animate-pulse' 'bg-amber-500 animate-pulse'
}`} /> }`} />
{data.status || '—'} {data.status || '—'}
@@ -595,15 +639,10 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
<button <button
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
updateRequestMutation.mutate({ handleMarkReceived(data);
tenantid,
locationid: locationid ?? FIESTA_PRIMARY_LOCATION_ID,
productid: Number(data.productid),
requestid: data.requestid,
status: 'Received'
});
}} }}
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-[11px] font-bold text-white bg-indigo-600 hover:bg-indigo-700 transition-colors shadow-sm" disabled={stockInMutation.isPending || updateRequestMutation.isPending}
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-[11px] font-bold text-white bg-indigo-600 hover:bg-indigo-700 transition-colors shadow-sm disabled:opacity-50 disabled:cursor-not-allowed"
> >
<CheckCircle2 size={14} /> Mark as Received <CheckCircle2 size={14} /> Mark as Received
</button> </button>
@@ -828,6 +867,7 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
const isPending = pick != null && pick.status === 'Pending'; const isPending = pick != null && pick.status === 'Pending';
const isApproved = pick != null && pick.status === 'Approved'; const isApproved = pick != null && pick.status === 'Approved';
const isRejected = pick != null && pick.status === 'Rejected'; const isRejected = pick != null && pick.status === 'Rejected';
const isReceived = pick != null && pick.status === 'Received';
const isRequested = pick != null && pick.status !== 'Cancelled'; const isRequested = pick != null && pick.status !== 'Cancelled';
return ( return (
@@ -869,10 +909,27 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
<div className="space-y-4 pt-4 border-t border-slate-100"> <div className="space-y-4 pt-4 border-t border-slate-100">
<h4 className="text-sm font-semibold text-slate-800">Stock Management</h4> <h4 className="text-sm font-semibold text-slate-800">Stock Management</h4>
{isApproved ? ( {isReceived ? (
<div className="flex items-center justify-center gap-2 p-4 bg-emerald-50 rounded-xl border border-emerald-200"> <div className="flex items-center justify-center gap-2 p-4 bg-indigo-50 rounded-xl border border-indigo-200">
<CheckCircle2 size={18} className="text-emerald-600" /> <CheckCircle2 size={18} className="text-indigo-600" />
<span className="text-sm font-semibold text-emerald-800">Approved and stocked in your inventory</span> <span className="text-sm font-semibold text-indigo-800">Received and stocked in your inventory</span>
</div>
) : isApproved ? (
<div className="flex flex-col gap-3">
<div className="flex items-center justify-center gap-2 p-4 bg-emerald-50 rounded-xl border border-emerald-200">
<CheckCircle2 size={18} className="text-emerald-600" />
<span className="text-sm font-semibold text-emerald-800">Approved <span className="font-bold">({pick.qty} units)</span> confirm once it arrives</span>
</div>
<button
onClick={() => {
setSelectedProduct(null);
handleMarkReceived({ productid: selectedProduct.id, qty: pick.qty, requestid: pick.requestid });
}}
disabled={stockInMutation.isPending || updateRequestMutation.isPending}
className="w-full flex items-center justify-center gap-2 py-3.5 rounded-xl bg-indigo-600 text-white hover:bg-indigo-700 font-semibold text-sm transition-colors shadow-sm disabled:opacity-50 disabled:cursor-not-allowed"
>
<CheckCircle2 size={18} /> Mark as Received
</button>
</div> </div>
) : isRejected ? ( ) : isRejected ? (
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">

View File

@@ -32,7 +32,7 @@ const NAV_ITEMS: SuperAdminNavItem[] = [
]; ];
export default function SuperAdminPage({ user, onLogout }: SuperAdminPageProps) { export default function SuperAdminPage({ user, onLogout }: SuperAdminPageProps) {
const [sidebarOpen, setSidebarOpen] = useState(true); const [sidebarOpen, setSidebarOpen] = useState(false);
const location = useLocation(); const location = useLocation();
const profile = { name: user.name, role: 'Super Admin', email: user.email }; const profile = { name: user.name, role: 'Super Admin', email: user.email };
const tenantId = user.tenantid || FIESTA_TENANT_ID; const tenantId = user.tenantid || FIESTA_TENANT_ID;

View File

@@ -64,7 +64,7 @@ type StoreShape = React.ComponentProps<typeof StoreDetailView>['store'];
* no other stores. Layout mirrors the admin console (fixed Header + left rail). * no other stores. Layout mirrors the admin console (fixed Header + left rail).
*/ */
export default function UserStorePage({ onLogout, user }: UserStorePageProps) { export default function UserStorePage({ onLogout, user }: UserStorePageProps) {
const [sidebarOpen, setSidebarOpen] = useState(true); const [sidebarOpen, setSidebarOpen] = useState(false);
const [showQrModal, setShowQrModal] = useState(false); const [showQrModal, setShowQrModal] = useState(false);
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();

View File

@@ -957,6 +957,7 @@ export function useFiestaCreateProductLocation() {
qc.invalidateQueries({ queryKey: ['catalogue', 'imported'] }); qc.invalidateQueries({ queryKey: ['catalogue', 'imported'] });
qc.invalidateQueries({ queryKey: ['fiesta', 'productLocations'] }); qc.invalidateQueries({ queryKey: ['fiesta', 'productLocations'] });
qc.invalidateQueries({ queryKey: ['fiesta', 'productStocks'] }); qc.invalidateQueries({ queryKey: ['fiesta', 'productStocks'] });
qc.invalidateQueries({ queryKey: ['fiesta', 'stockStatement'] });
}, },
onError: (error: any) => { onError: (error: any) => {
alert(`Error adding product: ${error.message}`); alert(`Error adding product: ${error.message}`);

View File

@@ -37,10 +37,10 @@ export interface StoreCatalogueItem {
* Live view of the store catalogue + curation helpers. Re-renders whenever the * Live view of the store catalogue + curation helpers. Re-renders whenever the
* catalogue changes via React Query invalidation. * catalogue changes via React Query invalidation.
*/ */
export function useStoreCatalogue() { export function useStoreCatalogue(tenantid: number = FIESTA_TENANT_ID, locationid: number = FIESTA_PRIMARY_LOCATION_ID) {
const q = useFiestaProductLocations({ const q = useFiestaProductLocations({
tenantid: FIESTA_TENANT_ID, tenantid,
locationid: FIESTA_PRIMARY_LOCATION_ID, locationid,
pagesize: 500, pagesize: 500,
}); });
@@ -64,8 +64,8 @@ export function useStoreCatalogue() {
const add = (item: StoreCatalogueItem) => { const add = (item: StoreCatalogueItem) => {
mutation.mutate({ mutation.mutate({
tenantid: FIESTA_TENANT_ID, tenantid,
locationid: FIESTA_PRIMARY_LOCATION_ID, locationid,
productid: Number(item.productid), productid: Number(item.productid),
qty: item.qty, qty: item.qty,
price: item.price, price: item.price,
@@ -77,8 +77,8 @@ export function useStoreCatalogue() {
const item = items.find(i => i.productid === id); const item = items.find(i => i.productid === id);
if (!item) return; if (!item) return;
deleteMutation.mutate({ deleteMutation.mutate({
tenantid: FIESTA_TENANT_ID, tenantid,
locationid: FIESTA_PRIMARY_LOCATION_ID, locationid,
productid: Number(id) productid: Number(id)
}); });
}; };
@@ -88,8 +88,8 @@ export function useStoreCatalogue() {
if (!item) return; if (!item) return;
const safeQty = Math.max(1, Math.round(qty) || 1); const safeQty = Math.max(1, Math.round(qty) || 1);
mutation.mutate({ mutation.mutate({
tenantid: FIESTA_TENANT_ID, tenantid,
locationid: FIESTA_PRIMARY_LOCATION_ID, locationid,
productid: Number(id), productid: Number(id),
qty: safeQty, qty: safeQty,
price: item.price, price: item.price,
@@ -101,8 +101,8 @@ export function useStoreCatalogue() {
const item = items.find(i => i.productid === id); const item = items.find(i => i.productid === id);
if (!item) return; if (!item) return;
mutation.mutate({ mutation.mutate({
tenantid: FIESTA_TENANT_ID, tenantid,
locationid: FIESTA_PRIMARY_LOCATION_ID, locationid,
productid: Number(id), productid: Number(id),
qty: item.qty || 0, qty: item.qty || 0,
price: price, price: price,