Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 19 additions & 13 deletions client/src/pages/dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,27 @@ export default function Dashboard() {
const [selectedCategory, setSelectedCategory] = useState<string>("all");
const { toast } = useToast();

const unwrapApiData = async <T,>(response: Response): Promise<T> => {
if (!response.ok) {
throw new Error(`Request failed (${response.status})`);
}
const raw = await response.json();
if (raw && typeof raw === "object" && "ok" in raw) {
const envelope = raw as { ok: boolean; data?: T; error?: { message?: string } };
if (envelope.ok) {
return envelope.data as T;
}
throw new Error(envelope.error?.message || "Request failed");
}
return raw as T;
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate unwrapApiData function across two pages

Low Severity

The unwrapApiData function is identically copy-pasted into both dashboard.tsx and reports.tsx as a component-scoped helper. No shared utility exists for this pattern in client/src/lib or elsewhere. Extracting it to a shared module would reduce maintenance burden and ensure consistent envelope-unwrapping behavior across all pages.

Additional Locations (1)

Fix in Cursor Fix in Web


// Fetch inventory stats
const { data: stats, isLoading: statsLoading } = useQuery({
queryKey: ["/api/inventory/stats"],
queryFn: async () => {
const response = await fetch("/api/inventory/stats");
if (!response.ok) {
throw new Error("Failed to fetch inventory stats");
}
return response.json() as Promise<InventoryStats>;
const response = await fetch("/api/inventory/stats", { credentials: "include" });
return unwrapApiData<InventoryStats>(response);
},
});

Expand All @@ -41,10 +53,7 @@ export default function Dashboard() {
queryKey: ["/api/categories"],
queryFn: async () => {
const response = await fetch("/api/categories");
if (!response.ok) {
throw new Error("Failed to fetch categories");
}
return response.json() as Promise<Category[]>;
return unwrapApiData<Category[]>(response);
},
});

Expand All @@ -57,10 +66,7 @@ export default function Dashboard() {
: "/api/inventory";

const response = await fetch(endpoint);
if (!response.ok) {
throw new Error("Failed to fetch inventory items");
}
return response.json() as Promise<InventoryItem[]>;
return unwrapApiData<InventoryItem[]>(response);
},
});

Expand Down
68 changes: 37 additions & 31 deletions client/src/pages/reports.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,23 @@ export default function Reports() {
const [filter, setFilter] = useState<ReportFilter>({});
const [exporting, setExporting] = useState(false);

const unwrapApiData = async <T,>(response: Response): Promise<T> => {
if (!response.ok) {
throw new Error(`Request failed (${response.status})`);
}

const raw = await response.json();
if (raw && typeof raw === "object" && "ok" in raw) {
const envelope = raw as { ok: boolean; data?: T; error?: { message?: string } };
if (envelope.ok) {
return envelope.data as T;
}
throw new Error(envelope.error?.message || "Request failed");
}

return raw as T;
};

// Fetch inventory items (normalize to array so reduce/map never see non-array)
const { data: inventoryItems, isLoading: itemsLoading, isError: itemsError, error: itemsErrorDetail } = useQuery({
queryKey: ["/api/inventory"],
Expand All @@ -50,11 +67,8 @@ export default function Reports() {
const { data: lowStockItems, isLoading: lowStockLoading } = useQuery({
queryKey: ["/api/inventory/low-stock"],
queryFn: async () => {
const response = await fetch("/api/inventory/low-stock");
if (!response.ok) {
throw new Error("Failed to fetch low stock items");
}
const raw = await response.json();
const response = await fetch("/api/inventory/low-stock", { credentials: "include" });
const raw = await unwrapApiData<unknown>(response);
return Array.isArray(raw) ? raw : [];
},
});
Expand All @@ -63,11 +77,8 @@ export default function Reports() {
const { data: categories } = useQuery({
queryKey: ["/api/categories"],
queryFn: async () => {
const response = await fetch("/api/categories");
if (!response.ok) {
throw new Error("Failed to fetch categories");
}
const raw = await response.json();
const response = await fetch("/api/categories", { credentials: "include" });
const raw = await unwrapApiData<unknown>(response);
return Array.isArray(raw) ? raw : [];
},
});
Expand All @@ -76,23 +87,23 @@ export default function Reports() {
const { data: stats } = useQuery({
queryKey: ["/api/inventory/stats"],
queryFn: async () => {
const response = await fetch("/api/inventory/stats");
if (!response.ok) {
throw new Error("Failed to fetch inventory stats");
}
return response.json() as Promise<InventoryStats>;
const response = await fetch("/api/inventory/stats", { credentials: "include" });
const rawStats = await unwrapApiData<Partial<InventoryStats>>(response);
return {
totalItems: Number(rawStats?.totalItems ?? 0),
lowStockItems: Number(rawStats?.lowStockItems ?? 0),
outOfStockItems: Number(rawStats?.outOfStockItems ?? 0),
inventoryValue: Number(rawStats?.inventoryValue ?? 0),
} as InventoryStats;
},
});

// Fetch warehouses for filtering (normalize to array)
const { data: warehouses } = useQuery({
queryKey: ["/api/warehouses"],
queryFn: async () => {
const response = await fetch("/api/warehouses");
if (!response.ok) {
throw new Error("Failed to fetch warehouses");
}
const raw = await response.json();
const response = await fetch("/api/warehouses", { credentials: "include" });
const raw = await unwrapApiData<unknown>(response);
return Array.isArray(raw) ? raw : [];
},
});
Expand All @@ -101,11 +112,8 @@ export default function Reports() {
const { data: suppliers } = useQuery({
queryKey: ["/api/suppliers"],
queryFn: async () => {
const response = await fetch("/api/suppliers");
if (!response.ok) {
throw new Error("Failed to fetch suppliers");
}
const raw = await response.json();
const response = await fetch("/api/suppliers", { credentials: "include" });
const raw = await unwrapApiData<unknown>(response);
return Array.isArray(raw) ? raw : [];
},
});
Expand Down Expand Up @@ -308,9 +316,7 @@ export default function Reports() {
</p>
</div>
<div className="text-sm text-neutral-600 dark:text-neutral-300">
{safeInventoryItems.length > 0
? `${safeInventoryItems.length} items • Total Value: ${formatCurrency(calculateTotalValue(safeInventoryItems))}`
: `${stats?.totalItems || 0} items • Total Value: ${formatCurrency(stats?.inventoryValue || 0)}`}
{`${safeInventoryItems.length} items • Total Value: ${formatCurrency(calculateTotalValue(safeInventoryItems))}`}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Report preview lost stats fallback during loading

Medium Severity

The report preview header and footer previously fell back to stats?.totalItems and stats?.inventoryValue when safeInventoryItems was empty (e.g., during loading or on fetch error). The new code always shows safeInventoryItems.length and calculateTotalValue(safeInventoryItems), which displays "0 items • Total Value: $0.00" whenever inventory items haven't loaded yet, even if stats are already available.

Additional Locations (1)

Fix in Cursor Fix in Web

</div>
</div>
<div className="overflow-x-auto">
Expand Down Expand Up @@ -348,10 +354,10 @@ export default function Reports() {
{item.quantity}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-neutral-600 dark:text-neutral-300">
{formatCurrency(item.price)}
{formatCurrency(Number(item.price) || 0)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-neutral-600 dark:text-neutral-300">
{formatCurrency(item.price * item.quantity)}
{formatCurrency((Number(item.price) || 0) * (Number(item.quantity) || 0))}
</td>
</tr>
))
Expand Down Expand Up @@ -392,7 +398,7 @@ export default function Reports() {
</CardContent>
<CardFooter className="bg-neutral-50 dark:bg-neutral-800 border-t border-neutral-200 dark:border-neutral-700 flex justify-between">
<div className="text-sm text-neutral-600 dark:text-neutral-300">
The complete report will include all {stats?.totalItems || 0} inventory items.
The complete report includes all {safeInventoryItems.length} inventory items from the current inventory feed.
</div>
</CardFooter>
</Card>
Expand Down
41 changes: 30 additions & 11 deletions client/src/pages/warehouses.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -154,16 +154,35 @@ export default function WarehousesPage() {
},
});

const handleCreateSubmit = (e: React.FormEvent) => {
e.preventDefault();
createWarehouse.mutate(formData);
const handleCreateSubmit = () => {
if (!formData.name.trim()) {
toast({
variant: 'destructive',
title: 'Warehouse name is required',
description: 'Enter a name before creating a warehouse.',
});
return;
}

createWarehouse.mutate({ ...formData, name: formData.name.trim() });
};

const handleEditSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (selectedWarehouse) {
updateWarehouse.mutate({ id: selectedWarehouse.id, data: formData });
const handleEditSubmit = () => {
if (!selectedWarehouse) return;

if (!formData.name.trim()) {
toast({
variant: 'destructive',
title: 'Warehouse name is required',
description: 'Enter a name before saving changes.',
});
return;
}

updateWarehouse.mutate({
id: selectedWarehouse.id,
data: { ...formData, name: formData.name.trim() },
});
};

const handleDeleteConfirm = () => {
Expand Down Expand Up @@ -317,7 +336,7 @@ export default function WarehousesPage() {
Enter the details for the new warehouse location.
</DialogDescription>
</DialogHeader>
<form onSubmit={handleCreateSubmit}>
<form noValidate>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="name">Warehouse Name *</Label>
Expand Down Expand Up @@ -385,7 +404,7 @@ export default function WarehousesPage() {
<Button variant="outline" type="button" onClick={() => setIsCreateDialogOpen(false)}>
Cancel
</Button>
<Button type="submit" disabled={createWarehouse.isPending}>
<Button type="button" onClick={handleCreateSubmit} disabled={createWarehouse.isPending}>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warehouse forms no longer submit on Enter key

Medium Severity

Both the create and edit warehouse forms replaced onSubmit handlers and type="submit" buttons with <form noValidate> (no onSubmit) and type="button" buttons with onClick. This removes the standard Enter-key-to-submit behavior. Users pressing Enter in any text input will no longer trigger handleCreateSubmit or handleEditSubmit, breaking a common form interaction pattern.

Additional Locations (1)

Fix in Cursor Fix in Web

{createWarehouse.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Create Warehouse
</Button>
Expand All @@ -403,7 +422,7 @@ export default function WarehousesPage() {
Update the warehouse details.
</DialogDescription>
</DialogHeader>
<form onSubmit={handleEditSubmit}>
<form noValidate>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="edit-name">Warehouse Name *</Label>
Expand Down Expand Up @@ -466,7 +485,7 @@ export default function WarehousesPage() {
<Button variant="outline" type="button" onClick={() => setIsEditDialogOpen(false)}>
Cancel
</Button>
<Button type="submit" disabled={updateWarehouse.isPending}>
<Button type="button" onClick={handleEditSubmit} disabled={updateWarehouse.isPending}>
{updateWarehouse.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Save Changes
</Button>
Expand Down
3 changes: 3 additions & 0 deletions server/operations-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,7 @@ export async function listOperationalInventory(filters: InventoryFilterInput) {
id: number;
sku: string;
name: string;
price: string | number | null;
category_id: number | null;
quantity: number | null;
low_stock_threshold: number | null;
Expand All @@ -416,6 +417,7 @@ export async function listOperationalInventory(filters: InventoryFilterInput) {
i.id,
i.sku,
i.name,
i.price,
i.category_id,
i.quantity,
i.low_stock_threshold,
Expand Down Expand Up @@ -488,6 +490,7 @@ export async function listOperationalInventory(filters: InventoryFilterInput) {
id: row.id,
sku: row.sku,
name: row.name,
price: toNumber(row.price, 0),
categoryId: row.category_id,
quantity: fallbackOnHand,
lowStockThreshold,
Expand Down
Loading