inventory page
This commit is contained in:
@@ -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(() => {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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}`);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user