diff --git a/CONSOLIDATION_PLAN.md b/CONSOLIDATION_PLAN.md index 7dac8e5..22fbde4 100644 --- a/CONSOLIDATION_PLAN.md +++ b/CONSOLIDATION_PLAN.md @@ -14,7 +14,7 @@ Audit-driven plan to (a) reduce 12 PM2 processes to 3 application servers + 1 au | 4 — Build `dashboard-server` (the merge) | Not started | klaviyo/meta/google/typeform still run as 4 separate PM2 apps | | 5 — Convert `acot-server` to ESM | Not started | | | 6 — Auth hardening | **Complete (code) — gated on Phase F1** | All in-process items wired (rate-limit, JWT precondition, CORS lockdown, request-log, upload allowlist, `requirePermission` on sensitive routes, permissions seed migration). `authenticate()` is live on `/api/*`. Server-side artefacts (Caddyfile, ecosystem.cjs) written to `inventory-server/deploy/` for review. 6.11 (audit logging) deferred. **Frontend cannot use the app until Phase F1 ships** — see below | -| **F1 — Frontend fetch wrapper (NEW)** | **Not started — CRITICAL** | Frontend uses raw `fetch()` in ~220 sites; only 7 send `Authorization: Bearer`. With Phase 6's `authenticate()` middleware live, every refresh 401s until the frontend uniformly attaches the token. See "Phase F1" below | +| **F1 — Frontend fetch wrapper (NEW)** | **Complete (code) — 2026-05-23** | Wrappers at `inventory/src/utils/api.ts` (`apiFetch`) and `inventory/src/utils/apiClient.ts` (axios instance). 170 `fetch()` sites across 76 files migrated to `apiFetch`; 32 `axios.*` sites across 11 files migrated to `apiClient`. AuthContext `/login`+`/me`, App.tsx `/me`, and `services/apiv2.ts` (external PHP backend) intentionally left as raw `fetch`. Type-check + production build pass clean | | 7 — Caddyfile final form | Partial | Proposed file at `inventory-server/deploy/Caddyfile.proposed`. Apply blocked on F1 (forward_auth would 401 every page load until then) | | 8 — ecosystem.config.cjs final form | Partial | Proposed at `inventory-server/deploy/ecosystem.config.cjs.proposed`. Includes Phase 6.4 JWT_SECRET footgun fix and 6.10 lt-wordlist token move | @@ -519,7 +519,7 @@ Already have `import-audit-log` and `product-editor-audit-log` tables. Extend th ## Phase F1 — Frontend fetch wrapper (NEW — 2026-05-23) -Status: **Not started. CRITICAL. Blocks the Phase 3+6 deploy from being usable.** +Status: **Complete (code) — 2026-05-23.** Two wrappers landed at `inventory/src/utils/api.ts` and `inventory/src/utils/apiClient.ts`. Migration touched 87 files (76 fetch, 11 axios) covering ~200 call sites. Type-check clean; production build clean. Intentional exclusions: AuthContext `/login`+`/me` (own auth flow), App.tsx initial `/me` session check, and `services/apiv2.ts` (calls the separate PHP backend at backend.acherryontop.com which has its own cookie auth, out of scope per the plan). Ready to ship in the same deploy window as Phase 3+6. ### The discovery diff --git a/inventory/src/components/analytics/AgingSellThrough.tsx b/inventory/src/components/analytics/AgingSellThrough.tsx index 85ed782..bb6b645 100644 --- a/inventory/src/components/analytics/AgingSellThrough.tsx +++ b/inventory/src/components/analytics/AgingSellThrough.tsx @@ -1,4 +1,5 @@ import { useQuery } from '@tanstack/react-query'; +import { apiFetch } from '@/utils/api'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { ResponsiveContainer, @@ -34,7 +35,7 @@ export function AgingSellThrough() { const { data, isLoading, isError } = useQuery({ queryKey: ['aging-sell-through'], queryFn: async () => { - const response = await fetch(`${config.apiUrl}/analytics/aging`); + const response = await apiFetch(`${config.apiUrl}/analytics/aging`); if (!response.ok) throw new Error('Failed to fetch aging data'); return response.json(); }, diff --git a/inventory/src/components/analytics/CapitalEfficiency.tsx b/inventory/src/components/analytics/CapitalEfficiency.tsx index c507aad..dd80e6d 100644 --- a/inventory/src/components/analytics/CapitalEfficiency.tsx +++ b/inventory/src/components/analytics/CapitalEfficiency.tsx @@ -1,4 +1,5 @@ import { useState } from 'react'; +import { apiFetch } from '@/utils/api'; import { useQuery } from '@tanstack/react-query'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { @@ -45,7 +46,7 @@ export function CapitalEfficiency() { const { data, isLoading, isError } = useQuery({ queryKey: ['capital-efficiency'], queryFn: async () => { - const response = await fetch(`${config.apiUrl}/analytics/efficiency`); + const response = await apiFetch(`${config.apiUrl}/analytics/efficiency`); if (!response.ok) throw new Error('Failed to fetch capital efficiency'); return response.json(); }, diff --git a/inventory/src/components/analytics/DiscountImpact.tsx b/inventory/src/components/analytics/DiscountImpact.tsx index 5f1afcf..646b33b 100644 --- a/inventory/src/components/analytics/DiscountImpact.tsx +++ b/inventory/src/components/analytics/DiscountImpact.tsx @@ -1,4 +1,5 @@ import { useQuery } from '@tanstack/react-query'; +import { apiFetch } from '@/utils/api'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { ResponsiveContainer, @@ -34,7 +35,7 @@ export function DiscountImpact() { const { data, isLoading, isError } = useQuery({ queryKey: ['discount-impact'], queryFn: async () => { - const response = await fetch(`${config.apiUrl}/analytics/discounts`); + const response = await apiFetch(`${config.apiUrl}/analytics/discounts`); if (!response.ok) throw new Error('Failed to fetch discount data'); return response.json(); }, diff --git a/inventory/src/components/analytics/GrowthMomentum.tsx b/inventory/src/components/analytics/GrowthMomentum.tsx index 71751da..c616473 100644 --- a/inventory/src/components/analytics/GrowthMomentum.tsx +++ b/inventory/src/components/analytics/GrowthMomentum.tsx @@ -1,4 +1,5 @@ import { useQuery } from '@tanstack/react-query'; +import { apiFetch } from '@/utils/api'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { ResponsiveContainer, @@ -55,7 +56,7 @@ export function GrowthMomentum() { const { data, isLoading, isError } = useQuery({ queryKey: ['growth-momentum'], queryFn: async () => { - const response = await fetch(`${config.apiUrl}/analytics/growth`); + const response = await apiFetch(`${config.apiUrl}/analytics/growth`); if (!response.ok) throw new Error('Failed to fetch growth data'); return response.json(); }, diff --git a/inventory/src/components/analytics/InventoryFlow.tsx b/inventory/src/components/analytics/InventoryFlow.tsx index 302745e..1c9f008 100644 --- a/inventory/src/components/analytics/InventoryFlow.tsx +++ b/inventory/src/components/analytics/InventoryFlow.tsx @@ -1,4 +1,5 @@ import { useState, useMemo } from 'react'; +import { apiFetch } from '@/utils/api'; import { useQuery } from '@tanstack/react-query'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { @@ -39,7 +40,7 @@ export function InventoryFlow() { const { data, isLoading, isError } = useQuery({ queryKey: ['inventory-flow', period], queryFn: async () => { - const response = await fetch(`${config.apiUrl}/analytics/flow?period=${period}`); + const response = await apiFetch(`${config.apiUrl}/analytics/flow?period=${period}`); if (!response.ok) throw new Error('Failed to fetch inventory flow'); return response.json(); }, diff --git a/inventory/src/components/analytics/InventoryTrends.tsx b/inventory/src/components/analytics/InventoryTrends.tsx index 4489cc0..07b01d8 100644 --- a/inventory/src/components/analytics/InventoryTrends.tsx +++ b/inventory/src/components/analytics/InventoryTrends.tsx @@ -1,4 +1,5 @@ import { useState } from 'react'; +import { apiFetch } from '@/utils/api'; import { useQuery } from '@tanstack/react-query'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { @@ -35,7 +36,7 @@ export function InventoryTrends() { const { data, isLoading, isError } = useQuery({ queryKey: ['inventory-trends', period], queryFn: async () => { - const response = await fetch(`${config.apiUrl}/analytics/inventory-trends?period=${period}`); + const response = await apiFetch(`${config.apiUrl}/analytics/inventory-trends?period=${period}`); if (!response.ok) throw new Error('Failed to fetch inventory trends'); return response.json(); }, diff --git a/inventory/src/components/analytics/InventoryValueTrend.tsx b/inventory/src/components/analytics/InventoryValueTrend.tsx index 2cc6905..78d4d75 100644 --- a/inventory/src/components/analytics/InventoryValueTrend.tsx +++ b/inventory/src/components/analytics/InventoryValueTrend.tsx @@ -1,4 +1,5 @@ import { useState } from 'react'; +import { apiFetch } from '@/utils/api'; import { useQuery } from '@tanstack/react-query'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { @@ -36,7 +37,7 @@ export function InventoryValueTrend() { const { data, isLoading, isError } = useQuery({ queryKey: ['inventory-value', period], queryFn: async () => { - const response = await fetch(`${config.apiUrl}/analytics/inventory-value?period=${period}`); + const response = await apiFetch(`${config.apiUrl}/analytics/inventory-value?period=${period}`); if (!response.ok) throw new Error('Failed to fetch inventory value'); return response.json(); }, diff --git a/inventory/src/components/analytics/PortfolioAnalysis.tsx b/inventory/src/components/analytics/PortfolioAnalysis.tsx index 02b26bc..5e5376a 100644 --- a/inventory/src/components/analytics/PortfolioAnalysis.tsx +++ b/inventory/src/components/analytics/PortfolioAnalysis.tsx @@ -1,4 +1,5 @@ import { useQuery } from '@tanstack/react-query'; +import { apiFetch } from '@/utils/api'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { ResponsiveContainer, @@ -42,7 +43,7 @@ export function PortfolioAnalysis() { const { data, isLoading, isError } = useQuery({ queryKey: ['portfolio-analysis'], queryFn: async () => { - const response = await fetch(`${config.apiUrl}/analytics/portfolio`); + const response = await apiFetch(`${config.apiUrl}/analytics/portfolio`); if (!response.ok) throw new Error('Failed to fetch portfolio analysis'); return response.json(); }, diff --git a/inventory/src/components/analytics/SeasonalPatterns.tsx b/inventory/src/components/analytics/SeasonalPatterns.tsx index e0e41dd..0ff6013 100644 --- a/inventory/src/components/analytics/SeasonalPatterns.tsx +++ b/inventory/src/components/analytics/SeasonalPatterns.tsx @@ -1,4 +1,5 @@ import { useQuery } from '@tanstack/react-query'; +import { apiFetch } from '@/utils/api'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { ResponsiveContainer, @@ -54,7 +55,7 @@ export function SeasonalPatterns() { const { data, isLoading, isError } = useQuery({ queryKey: ['seasonal-patterns'], queryFn: async () => { - const response = await fetch(`${config.apiUrl}/analytics/seasonal`); + const response = await apiFetch(`${config.apiUrl}/analytics/seasonal`); if (!response.ok) throw new Error('Failed to fetch seasonal data'); return response.json(); }, diff --git a/inventory/src/components/analytics/StockHealth.tsx b/inventory/src/components/analytics/StockHealth.tsx index 6c631cb..ef80b7b 100644 --- a/inventory/src/components/analytics/StockHealth.tsx +++ b/inventory/src/components/analytics/StockHealth.tsx @@ -1,4 +1,5 @@ import { useQuery } from '@tanstack/react-query'; +import { apiFetch } from '@/utils/api'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { ResponsiveContainer, @@ -72,7 +73,7 @@ export function StockHealth() { const { data, isLoading, isError } = useQuery({ queryKey: ['stock-health'], queryFn: async () => { - const response = await fetch(`${config.apiUrl}/analytics/stock-health`); + const response = await apiFetch(`${config.apiUrl}/analytics/stock-health`); if (!response.ok) throw new Error('Failed to fetch stock health'); return response.json(); }, diff --git a/inventory/src/components/analytics/StockoutRisk.tsx b/inventory/src/components/analytics/StockoutRisk.tsx index ed7392e..7c85549 100644 --- a/inventory/src/components/analytics/StockoutRisk.tsx +++ b/inventory/src/components/analytics/StockoutRisk.tsx @@ -1,4 +1,5 @@ import { useQuery } from '@tanstack/react-query'; +import { apiFetch } from '@/utils/api'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { ResponsiveContainer, @@ -49,7 +50,7 @@ export function StockoutRisk() { const { data, isLoading, isError } = useQuery({ queryKey: ['stockout-risk'], queryFn: async () => { - const response = await fetch(`${config.apiUrl}/analytics/stockout-risk`); + const response = await apiFetch(`${config.apiUrl}/analytics/stockout-risk`); if (!response.ok) throw new Error('Failed to fetch stockout risk'); return response.json(); }, diff --git a/inventory/src/components/chat/ChatRoom.tsx b/inventory/src/components/chat/ChatRoom.tsx index 0a6e9f6..ba890f0 100644 --- a/inventory/src/components/chat/ChatRoom.tsx +++ b/inventory/src/components/chat/ChatRoom.tsx @@ -1,4 +1,5 @@ import { useState, useEffect, useRef } from 'react'; +import { apiFetch } from '@/utils/api'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; @@ -73,7 +74,7 @@ export function ChatRoom({ roomId, selectedUserId }: ChatRoomProps) { const fetchRoom = async () => { try { - const response = await fetch(`${config.chatUrl}/rooms/${roomId}?userId=${selectedUserId}`); + const response = await apiFetch(`${config.chatUrl}/rooms/${roomId}?userId=${selectedUserId}`); const data = await response.json(); if (data.status === 'success') { @@ -101,7 +102,7 @@ export function ChatRoom({ roomId, selectedUserId }: ChatRoomProps) { params.set('before', before); } - const response = await fetch(`${config.chatUrl}/rooms/${roomId}/messages?${params}`); + const response = await apiFetch(`${config.chatUrl}/rooms/${roomId}/messages?${params}`); const data = await response.json(); if (data.status === 'success') { @@ -136,7 +137,7 @@ export function ChatRoom({ roomId, selectedUserId }: ChatRoomProps) { const loadAttachments = async (messageIds: number[]) => { try { - const response = await fetch(`${config.chatUrl}/messages/attachments`, { + const response = await apiFetch(`${config.chatUrl}/messages/attachments`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -172,7 +173,7 @@ export function ChatRoom({ roomId, selectedUserId }: ChatRoomProps) { if (!searchQuery || searchQuery.length < 2) return; try { - const response = await fetch( + const response = await apiFetch( `${config.chatUrl}/users/${selectedUserId}/search?q=${encodeURIComponent(searchQuery)}&limit=20` ); const data = await response.json(); @@ -554,4 +555,4 @@ export function ChatRoom({ roomId, selectedUserId }: ChatRoomProps) { ); -} \ No newline at end of file +} diff --git a/inventory/src/components/chat/ChatTest.tsx b/inventory/src/components/chat/ChatTest.tsx index 82627c8..53bfd68 100644 --- a/inventory/src/components/chat/ChatTest.tsx +++ b/inventory/src/components/chat/ChatTest.tsx @@ -1,4 +1,5 @@ import { useState, useEffect } from 'react'; +import { apiFetch } from '@/utils/api'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Loader2, Hash, Lock, Users, MessageSquare } from 'lucide-react'; @@ -33,7 +34,7 @@ export function ChatTest({ selectedUserId }: ChatTestProps) { setError(null); try { - const response = await fetch(`${config.chatUrl}/users/${selectedUserId}/rooms`); + const response = await apiFetch(`${config.chatUrl}/users/${selectedUserId}/rooms`); const data = await response.json(); if (data.status === 'success') { @@ -174,4 +175,4 @@ export function ChatTest({ selectedUserId }: ChatTestProps) { ); -} \ No newline at end of file +} diff --git a/inventory/src/components/chat/RoomList.tsx b/inventory/src/components/chat/RoomList.tsx index 44f4b2a..582e8ed 100644 --- a/inventory/src/components/chat/RoomList.tsx +++ b/inventory/src/components/chat/RoomList.tsx @@ -1,4 +1,5 @@ import React, { useState, useEffect } from 'react'; +import { apiFetch } from '@/utils/api'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Loader2, Hash, Users, MessageSquare, MessageCircle, Users2 } from 'lucide-react'; @@ -50,7 +51,7 @@ export function RoomList({ selectedUserId, selectedRoomId, onRoomSelect }: RoomL setError(null); try { - const response = await fetch(`${config.chatUrl}/users/${selectedUserId}/rooms`); + const response = await apiFetch(`${config.chatUrl}/users/${selectedUserId}/rooms`); const data = await response.json(); if (data.status === 'success') { @@ -327,4 +328,4 @@ export function RoomList({ selectedUserId, selectedRoomId, onRoomSelect }: RoomL ); -} \ No newline at end of file +} diff --git a/inventory/src/components/create-po/AddProductsDialog.tsx b/inventory/src/components/create-po/AddProductsDialog.tsx index 2ddee66..54eb92c 100644 --- a/inventory/src/components/create-po/AddProductsDialog.tsx +++ b/inventory/src/components/create-po/AddProductsDialog.tsx @@ -53,7 +53,7 @@ import { X, } from "lucide-react"; import { toast } from "sonner"; -import axios from "axios"; +import { apiClient } from "@/utils/apiClient"; import type { SearchProduct } from "@/components/product-editor/types"; import { parsePastedTable, @@ -165,7 +165,7 @@ export function AddProductsDialog({ setIsSearching(true); setSearched(true); try { - const res = await axios.get<{ results: QuickSearchResult[]; total: number }>( + const res = await apiClient.get<{ results: QuickSearchResult[]; total: number }>( "/api/products/search", { params: { q: query } } ); @@ -182,7 +182,7 @@ export function AddProductsDialog({ async (result: QuickSearchResult) => { // Look up full details to pull MOQ (to default qty sensibly) try { - const res = await axios.get("/api/import/search-products", { + const res = await apiClient.get("/api/import/search-products", { params: { pid: result.pid }, }); const full = (res.data ?? [])[0]; @@ -204,7 +204,7 @@ export function AddProductsDialog({ if (pids.length === 0) return; try { - const res = await axios.get("/api/import/search-products", { + const res = await apiClient.get("/api/import/search-products", { params: { pid: pids.join(",") }, }); const items = (res.data ?? []).map((p) => ({ diff --git a/inventory/src/components/create-po/SupplierSelector.tsx b/inventory/src/components/create-po/SupplierSelector.tsx index 6df82c9..7895db8 100644 --- a/inventory/src/components/create-po/SupplierSelector.tsx +++ b/inventory/src/components/create-po/SupplierSelector.tsx @@ -9,6 +9,7 @@ */ import { useQuery } from "@tanstack/react-query"; +import { apiFetch } from '@/utils/api'; import { ComboboxField } from "@/components/product-editor/ComboboxField"; import { Skeleton } from "@/components/ui/skeleton"; import type { FieldOption } from "@/components/product-editor/types"; @@ -30,7 +31,7 @@ export function SupplierSelector({ const { data, isLoading, error } = useQuery({ queryKey: ["field-options"], queryFn: async (): Promise => { - const res = await fetch("/api/import/field-options"); + const res = await apiFetch("/api/import/field-options"); if (!res.ok) throw new Error("Failed to load suppliers"); return res.json(); }, diff --git a/inventory/src/components/create-po/resolveIdentifiers.ts b/inventory/src/components/create-po/resolveIdentifiers.ts index f8be4db..d2f12c0 100644 --- a/inventory/src/components/create-po/resolveIdentifiers.ts +++ b/inventory/src/components/create-po/resolveIdentifiers.ts @@ -16,7 +16,7 @@ * full PoLineItem rows via `GET /api/products/batch`. */ -import axios from "axios"; +import { apiClient } from "@/utils/apiClient"; import type { RawIdentifierRow, ResolveResult, @@ -66,7 +66,7 @@ export async function resolveIdentifiers( const identifiers = rows.map((r) => r.identifier); - const res = await axios.post( + const res = await apiClient.post( "/api/products/resolve-identifiers", { identifiers } ); @@ -120,7 +120,7 @@ export async function fetchBatchProducts( for (let i = 0; i < uniqPids.length; i += BATCH_LOOKUP_MAX_PIDS) { const chunk = uniqPids.slice(i, i + BATCH_LOOKUP_MAX_PIDS); - const res = await axios.get[]>( + const res = await apiClient.get[]>( "/api/products/batch", { params: { pids: chunk.join(",") } } ); diff --git a/inventory/src/components/newsletter/CampaignHistoryDialog.tsx b/inventory/src/components/newsletter/CampaignHistoryDialog.tsx index 3419e7d..ca97b55 100644 --- a/inventory/src/components/newsletter/CampaignHistoryDialog.tsx +++ b/inventory/src/components/newsletter/CampaignHistoryDialog.tsx @@ -1,4 +1,5 @@ import { useState, useMemo } from "react" +import { apiFetch } from '@/utils/api'; import { useQuery } from "@tanstack/react-query" import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, @@ -18,7 +19,7 @@ function useCampaignData(open: boolean) { const campaigns = useQuery({ queryKey: ["newsletter-campaigns"], queryFn: async () => { - const res = await fetch(`${config.apiUrl}/newsletter/campaigns`) + const res = await apiFetch(`${config.apiUrl}/newsletter/campaigns`) if (!res.ok) throw new Error("Failed to fetch campaigns") return res.json() }, @@ -29,7 +30,7 @@ function useCampaignData(open: boolean) { const products = useQuery<{ products: ProductAggregate[] }>({ queryKey: ["newsletter-campaigns-products"], queryFn: async () => { - const res = await fetch(`${config.apiUrl}/newsletter/campaigns/products`) + const res = await apiFetch(`${config.apiUrl}/newsletter/campaigns/products`) if (!res.ok) throw new Error("Failed to fetch") return res.json() }, @@ -40,7 +41,7 @@ function useCampaignData(open: boolean) { const links = useQuery<{ links: LinkAggregate[] }>({ queryKey: ["newsletter-campaigns-links"], queryFn: async () => { - const res = await fetch(`${config.apiUrl}/newsletter/campaigns/links`) + const res = await apiFetch(`${config.apiUrl}/newsletter/campaigns/links`) if (!res.ok) throw new Error("Failed to fetch") return res.json() }, @@ -51,7 +52,7 @@ function useCampaignData(open: boolean) { const brands = useQuery<{ brands: BrandAggregate[] }>({ queryKey: ["newsletter-campaigns-brands"], queryFn: async () => { - const res = await fetch(`${config.apiUrl}/newsletter/campaigns/brands`) + const res = await apiFetch(`${config.apiUrl}/newsletter/campaigns/brands`) if (!res.ok) throw new Error("Failed to fetch") return res.json() }, diff --git a/inventory/src/components/newsletter/NewsletterStats.tsx b/inventory/src/components/newsletter/NewsletterStats.tsx index 713a29a..75f9152 100644 --- a/inventory/src/components/newsletter/NewsletterStats.tsx +++ b/inventory/src/components/newsletter/NewsletterStats.tsx @@ -1,4 +1,5 @@ import { useQuery } from "@tanstack/react-query" +import { apiFetch } from '@/utils/api'; import { Card, CardContent } from "@/components/ui/card" import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" import { Skeleton } from "@/components/ui/skeleton" @@ -18,7 +19,7 @@ export function NewsletterStats() { const { data } = useQuery({ queryKey: ["newsletter-stats"], queryFn: async () => { - const res = await fetch(`${config.apiUrl}/newsletter/stats`) + const res = await apiFetch(`${config.apiUrl}/newsletter/stats`) if (!res.ok) throw new Error("Failed to fetch stats") return res.json() }, diff --git a/inventory/src/components/newsletter/RecommendationTable.tsx b/inventory/src/components/newsletter/RecommendationTable.tsx index a6a4d60..cebf642 100644 --- a/inventory/src/components/newsletter/RecommendationTable.tsx +++ b/inventory/src/components/newsletter/RecommendationTable.tsx @@ -1,4 +1,5 @@ import { useQuery } from "@tanstack/react-query" +import { apiFetch } from '@/utils/api'; import { useState, useMemo, useContext } from "react" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, @@ -170,7 +171,7 @@ function ScoreBreakdownTooltip({ pid, score, children }: { pid: number; score: n const { data } = useQuery({ queryKey: ["score-breakdown", pid], queryFn: async () => { - const res = await fetch(`${config.apiUrl}/newsletter/score-breakdown/${pid}`) + const res = await apiFetch(`${config.apiUrl}/newsletter/score-breakdown/${pid}`) if (!res.ok) throw new Error("Failed") return res.json() }, @@ -246,7 +247,7 @@ export function RecommendationTable({ category }: RecommendationTableProps) { const { data, isLoading } = useQuery({ queryKey: ["newsletter-recommendations", category, page], queryFn: async () => { - const res = await fetch( + const res = await apiFetch( `${config.apiUrl}/newsletter/recommendations?category=${category}&page=${page}&limit=${limit}` ) if (!res.ok) throw new Error("Failed to fetch recommendations") diff --git a/inventory/src/components/overview/BestSellers.tsx b/inventory/src/components/overview/BestSellers.tsx index ba905d3..28b6c79 100644 --- a/inventory/src/components/overview/BestSellers.tsx +++ b/inventory/src/components/overview/BestSellers.tsx @@ -1,4 +1,5 @@ import { useQuery } from "@tanstack/react-query" +import { apiFetch } from '@/utils/api'; import { CardHeader, CardTitle, CardContent } from "@/components/ui/card" import { ScrollArea } from "@/components/ui/scroll-area" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" @@ -52,7 +53,7 @@ export function BestSellers() { const { data, isError, isLoading } = useQuery({ queryKey: ["best-sellers"], queryFn: async () => { - const response = await fetch(`${config.apiUrl}/dashboard/best-sellers`) + const response = await apiFetch(`${config.apiUrl}/dashboard/best-sellers`) if (!response.ok) throw new Error("Failed to fetch best sellers"); return response.json() }, diff --git a/inventory/src/components/overview/ForecastAccuracy.tsx b/inventory/src/components/overview/ForecastAccuracy.tsx index f3a9a18..c0c3d9a 100644 --- a/inventory/src/components/overview/ForecastAccuracy.tsx +++ b/inventory/src/components/overview/ForecastAccuracy.tsx @@ -1,4 +1,5 @@ import { useQuery } from "@tanstack/react-query" +import { apiFetch } from '@/utils/api'; import { BarChart, Bar, ResponsiveContainer, XAxis, YAxis, Tooltip as RechartsTooltip, Cell, LineChart, Line } from "recharts" import config from "@/config" import { Target, TrendingDown, ArrowUpDown } from "lucide-react" @@ -83,7 +84,7 @@ export function ForecastAccuracy() { const { data, error, isLoading } = useQuery({ queryKey: ["forecast-accuracy"], queryFn: async () => { - const response = await fetch(`${config.apiUrl}/dashboard/forecast/accuracy`) + const response = await apiFetch(`${config.apiUrl}/dashboard/forecast/accuracy`) if (!response.ok) { throw new Error("Failed to fetch forecast accuracy") } diff --git a/inventory/src/components/overview/ForecastMetrics.tsx b/inventory/src/components/overview/ForecastMetrics.tsx index 5c04b05..32a67ee 100644 --- a/inventory/src/components/overview/ForecastMetrics.tsx +++ b/inventory/src/components/overview/ForecastMetrics.tsx @@ -1,4 +1,5 @@ import { useQuery } from "@tanstack/react-query" +import { apiFetch } from '@/utils/api'; import { CardHeader, CardTitle, CardContent } from "@/components/ui/card" import { AreaChart, Area, ResponsiveContainer, XAxis, YAxis, Tooltip as RechartsTooltip } from "recharts" import { useState } from "react" @@ -72,7 +73,7 @@ export function ForecastMetrics() { startDate: new Date().toISOString(), endDate: getEndDate(period).toISOString(), }); - const response = await fetch(`${config.apiUrl}/dashboard/forecast/metrics?${params}`) + const response = await apiFetch(`${config.apiUrl}/dashboard/forecast/metrics?${params}`) if (!response.ok) { const text = await response.text(); throw new Error(`Failed to fetch forecast metrics: ${text}`); diff --git a/inventory/src/components/overview/OverstockMetrics.tsx b/inventory/src/components/overview/OverstockMetrics.tsx index 3d4e006..4b5f9fd 100644 --- a/inventory/src/components/overview/OverstockMetrics.tsx +++ b/inventory/src/components/overview/OverstockMetrics.tsx @@ -1,4 +1,5 @@ import { useQuery } from "@tanstack/react-query" +import { apiFetch } from '@/utils/api'; import { CardHeader, CardTitle, CardContent } from "@/components/ui/card" import config from "@/config" import { formatCurrency } from "@/utils/formatCurrency" @@ -38,7 +39,7 @@ export function OverstockMetrics() { const { data, isError, isLoading } = useQuery({ queryKey: ["overstock-metrics"], queryFn: async () => { - const response = await fetch(`${config.apiUrl}/dashboard/overstock/metrics`) + const response = await apiFetch(`${config.apiUrl}/dashboard/overstock/metrics`) if (!response.ok) throw new Error('Failed to fetch overstock metrics'); return response.json(); }, diff --git a/inventory/src/components/overview/PurchaseMetrics.tsx b/inventory/src/components/overview/PurchaseMetrics.tsx index 981e541..2e1d6e4 100644 --- a/inventory/src/components/overview/PurchaseMetrics.tsx +++ b/inventory/src/components/overview/PurchaseMetrics.tsx @@ -1,4 +1,5 @@ import { useQuery } from "@tanstack/react-query" +import { apiFetch } from '@/utils/api'; import { CardHeader, CardTitle, CardContent } from "@/components/ui/card" import { PieChart, Pie, ResponsiveContainer, Cell, Sector } from "recharts" import config from "@/config" @@ -75,7 +76,7 @@ export function PurchaseMetrics() { const { data, isError, isLoading } = useQuery({ queryKey: ["purchase-metrics"], queryFn: async () => { - const response = await fetch(`${config.apiUrl}/dashboard/purchase/metrics`) + const response = await apiFetch(`${config.apiUrl}/dashboard/purchase/metrics`) if (!response.ok) throw new Error('Failed to fetch purchase metrics'); return response.json(); }, diff --git a/inventory/src/components/overview/ReplenishmentMetrics.tsx b/inventory/src/components/overview/ReplenishmentMetrics.tsx index 3e94644..dd49d36 100644 --- a/inventory/src/components/overview/ReplenishmentMetrics.tsx +++ b/inventory/src/components/overview/ReplenishmentMetrics.tsx @@ -1,4 +1,5 @@ import { useQuery } from "@tanstack/react-query" +import { apiFetch } from '@/utils/api'; import { CardHeader, CardTitle, CardContent } from "@/components/ui/card" import config from "@/config" import { formatCurrency } from "@/utils/formatCurrency" @@ -40,7 +41,7 @@ export function ReplenishmentMetrics() { const { data, isError, isLoading } = useQuery({ queryKey: ["replenishment-metrics"], queryFn: async () => { - const response = await fetch(`${config.apiUrl}/dashboard/replenishment/metrics`) + const response = await apiFetch(`${config.apiUrl}/dashboard/replenishment/metrics`) if (!response.ok) throw new Error('Failed to fetch replenishment metrics'); return response.json(); }, diff --git a/inventory/src/components/overview/SalesMetrics.tsx b/inventory/src/components/overview/SalesMetrics.tsx index 9d6296e..e30fa25 100644 --- a/inventory/src/components/overview/SalesMetrics.tsx +++ b/inventory/src/components/overview/SalesMetrics.tsx @@ -1,4 +1,5 @@ import { useQuery } from "@tanstack/react-query" +import { apiFetch } from '@/utils/api'; import { CardHeader, CardTitle, CardContent } from "@/components/ui/card" import { AreaChart, Area, ResponsiveContainer, XAxis, YAxis, Tooltip as RechartsTooltip } from "recharts" import { useState } from "react" @@ -61,7 +62,7 @@ export function SalesMetrics() { startDate: addDays(new Date(), -period).toISOString(), endDate: new Date().toISOString(), }); - const response = await fetch(`${config.apiUrl}/dashboard/sales/metrics?${params}`) + const response = await apiFetch(`${config.apiUrl}/dashboard/sales/metrics?${params}`) if (!response.ok) throw new Error("Failed to fetch sales metrics"); return response.json() }, diff --git a/inventory/src/components/overview/StockMetrics.tsx b/inventory/src/components/overview/StockMetrics.tsx index c09d0b9..7e9dea8 100644 --- a/inventory/src/components/overview/StockMetrics.tsx +++ b/inventory/src/components/overview/StockMetrics.tsx @@ -1,4 +1,5 @@ import { useQuery } from "@tanstack/react-query" +import { apiFetch } from '@/utils/api'; import { CardHeader, CardTitle, CardContent } from "@/components/ui/card" import { PieChart, Pie, ResponsiveContainer, Cell, Sector } from "recharts" import config from "@/config" @@ -107,7 +108,7 @@ export function StockMetrics() { const { data, isError, isLoading } = useQuery({ queryKey: ["stock-metrics"], queryFn: async () => { - const response = await fetch(`${config.apiUrl}/dashboard/stock/metrics`); + const response = await apiFetch(`${config.apiUrl}/dashboard/stock/metrics`); if (!response.ok) throw new Error('Failed to fetch stock metrics'); return response.json(); }, diff --git a/inventory/src/components/overview/TopOverstockedProducts.tsx b/inventory/src/components/overview/TopOverstockedProducts.tsx index d913fc2..e79a108 100644 --- a/inventory/src/components/overview/TopOverstockedProducts.tsx +++ b/inventory/src/components/overview/TopOverstockedProducts.tsx @@ -1,4 +1,5 @@ import { useQuery } from "@tanstack/react-query" +import { apiFetch } from '@/utils/api'; import { CardHeader, CardTitle, CardContent } from "@/components/ui/card" import { ScrollArea } from "@/components/ui/scroll-area" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" @@ -29,7 +30,7 @@ export function TopOverstockedProducts() { const { data, isError, isLoading } = useQuery({ queryKey: ["top-overstocked-products"], queryFn: async () => { - const response = await fetch(`${config.apiUrl}/dashboard/overstock/products?limit=50`) + const response = await apiFetch(`${config.apiUrl}/dashboard/overstock/products?limit=50`) if (!response.ok) throw new Error("Failed to fetch overstocked products"); return response.json() }, diff --git a/inventory/src/components/overview/TopReplenishProducts.tsx b/inventory/src/components/overview/TopReplenishProducts.tsx index 585833f..d2460b2 100644 --- a/inventory/src/components/overview/TopReplenishProducts.tsx +++ b/inventory/src/components/overview/TopReplenishProducts.tsx @@ -1,4 +1,5 @@ import { useQuery } from "@tanstack/react-query" +import { apiFetch } from '@/utils/api'; import { CardHeader, CardTitle, CardContent } from "@/components/ui/card" import { ScrollArea } from "@/components/ui/scroll-area" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" @@ -29,7 +30,7 @@ export function TopReplenishProducts() { const { data, isError, isLoading } = useQuery({ queryKey: ["top-replenish-products"], queryFn: async () => { - const response = await fetch(`${config.apiUrl}/dashboard/replenish/products?limit=50`) + const response = await apiFetch(`${config.apiUrl}/dashboard/replenish/products?limit=50`) if (!response.ok) throw new Error("Failed to fetch products to replenish"); return response.json() }, diff --git a/inventory/src/components/product-editor/ImageManager.tsx b/inventory/src/components/product-editor/ImageManager.tsx index bce9888..91033c0 100644 --- a/inventory/src/components/product-editor/ImageManager.tsx +++ b/inventory/src/components/product-editor/ImageManager.tsx @@ -1,5 +1,5 @@ import { useState, useCallback, useRef } from "react"; -import axios from "axios"; +import { apiClient } from "@/utils/apiClient"; import { toast } from "sonner"; import { useDropzone } from "react-dropzone"; import { Input } from "@/components/ui/input"; @@ -305,7 +305,7 @@ export function ImageManager({ try { const formData = new FormData(); formData.append("image", file); - const res = await axios.post("/api/import/upload-image", formData); + const res = await apiClient.post("/api/import/upload-image", formData); if (res.data?.imageUrl) { addNewImage(res.data.imageUrl); } @@ -356,7 +356,7 @@ export function ImageManager({ setAddOpen(false); try { - const res = await axios.post("/api/import/upload-image-url", { + const res = await apiClient.post("/api/import/upload-image-url", { imageUrl: normalizeImageUrlInput(url), }); if (res.data?.imageUrl) { diff --git a/inventory/src/components/product-editor/ProductEditForm.tsx b/inventory/src/components/product-editor/ProductEditForm.tsx index ba62b1f..2fefba8 100644 --- a/inventory/src/components/product-editor/ProductEditForm.tsx +++ b/inventory/src/components/product-editor/ProductEditForm.tsx @@ -1,5 +1,6 @@ import { useState, useEffect, useCallback, useRef, useContext } from "react"; import axios from "axios"; +import { apiClient } from "@/utils/apiClient"; import { useForm, Controller } from "react-hook-form"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; @@ -499,7 +500,7 @@ export function ProductEditForm({ originalValuesRef.current = { ...data }; if (imageChanges) { try { - const res = await axios.get(`/api/import/product-images/${product.pid}`); + const res = await apiClient.get(`/api/import/product-images/${product.pid}`); originalImagesRef.current = res.data; setProductImages(res.data); } catch { diff --git a/inventory/src/components/product-editor/ProductSearch.tsx b/inventory/src/components/product-editor/ProductSearch.tsx index 8da3948..f7e8250 100644 --- a/inventory/src/components/product-editor/ProductSearch.tsx +++ b/inventory/src/components/product-editor/ProductSearch.tsx @@ -1,5 +1,5 @@ import { useState, useCallback } from "react"; -import axios from "axios"; +import { apiClient } from "@/utils/apiClient"; import { toast } from "sonner"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; @@ -57,11 +57,11 @@ export function ProductSearch({ setIsSearching(true); onNewSearch(); try { - const res = await axios.get("/api/products/search", { + const res = await apiClient.get("/api/products/search", { params: { q: searchTerm }, }); if (res.data.total === 0) { - const fallback = await axios.get("/api/import/search-products", { + const fallback = await apiClient.get("/api/import/search-products", { params: { q: searchTerm }, }); const fullProducts = fallback.data as SearchProduct[]; @@ -102,7 +102,7 @@ export function ProductSearch({ } setIsLoadingProduct(product.pid); try { - const res = await axios.get("/api/import/search-products", { + const res = await apiClient.get("/api/import/search-products", { params: { pid: product.pid }, }); const full = (res.data as SearchProduct[])[0]; diff --git a/inventory/src/components/product-editor/useProductSuggestions.ts b/inventory/src/components/product-editor/useProductSuggestions.ts index fd32d55..2cc4c1b 100644 --- a/inventory/src/components/product-editor/useProductSuggestions.ts +++ b/inventory/src/components/product-editor/useProductSuggestions.ts @@ -11,6 +11,7 @@ */ import { useState, useRef, useCallback, useEffect } from 'react'; +import { apiFetch } from '@/utils/api'; import type { TaxonomySuggestion, ProductSuggestions } from '@/components/product-import/steps/ValidationStep/store/types'; const API_BASE = '/api/ai'; @@ -20,7 +21,7 @@ let initPromise: Promise | null = null; async function ensureInitialized(): Promise { if (!initPromise) { - initPromise = fetch(`${API_BASE}/initialize`, { method: 'POST' }) + initPromise = apiFetch(`${API_BASE}/initialize`, { method: 'POST' }) .then((r) => r.json()) .then((d) => Boolean(d.success)) .catch(() => { @@ -81,7 +82,7 @@ export function useProductSuggestions(product: ProductInput): ProductSuggestionR return; } - const response = await fetch(`${API_BASE}/suggestions`, { + const response = await apiFetch(`${API_BASE}/suggestions`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ product: p }), diff --git a/inventory/src/components/product-import/CreateProductCategoryDialog.tsx b/inventory/src/components/product-import/CreateProductCategoryDialog.tsx index b90331c..7a80480 100644 --- a/inventory/src/components/product-import/CreateProductCategoryDialog.tsx +++ b/inventory/src/components/product-import/CreateProductCategoryDialog.tsx @@ -1,4 +1,5 @@ import { useCallback, useContext, useEffect, useMemo, useState } from "react"; +import { apiFetch } from '@/utils/api'; import { Check, ChevronsUpDown, Loader2 } from "lucide-react"; import { toast } from "sonner"; @@ -137,7 +138,7 @@ export function CreateProductCategoryDialog({ setIsLoadingLines(true); try { - const response = await fetch(`${config.apiUrl}/import/product-lines/${targetCompanyId}`); + const response = await apiFetch(`${config.apiUrl}/import/product-lines/${targetCompanyId}`); if (!response.ok) { throw new Error("Failed to load product lines"); } diff --git a/inventory/src/components/product-import/steps/ImageUploadStep/components/ProductCard/ProductCard.tsx b/inventory/src/components/product-import/steps/ImageUploadStep/components/ProductCard/ProductCard.tsx index 6e00b15..39e19fa 100644 --- a/inventory/src/components/product-import/steps/ImageUploadStep/components/ProductCard/ProductCard.tsx +++ b/inventory/src/components/product-import/steps/ImageUploadStep/components/ProductCard/ProductCard.tsx @@ -1,4 +1,5 @@ import { Button } from "@/components/ui/button"; +import { apiFetch } from '@/utils/api'; import { Input } from "@/components/ui/input"; import { Card, CardContent } from "@/components/ui/card"; import { Loader2, Link as LinkIcon, Image as ImageIcon } from "lucide-react"; @@ -83,7 +84,7 @@ export const ProductCard = ({ const { data: reusableImages, isLoading: isLoadingReusable } = useQuery({ queryKey: ["reusable-images"], queryFn: async () => { - const response = await fetch(`${config.apiUrl}/reusable-images`); + const response = await apiFetch(`${config.apiUrl}/reusable-images`); if (!response.ok) { throw new Error("Failed to fetch reusable images"); } @@ -265,4 +266,4 @@ export const ProductCard = ({ ); -}; \ No newline at end of file +}; diff --git a/inventory/src/components/product-import/steps/ImageUploadStep/hooks/useProductImageOperations.ts b/inventory/src/components/product-import/steps/ImageUploadStep/hooks/useProductImageOperations.ts index 03944ef..afb27ba 100644 --- a/inventory/src/components/product-import/steps/ImageUploadStep/hooks/useProductImageOperations.ts +++ b/inventory/src/components/product-import/steps/ImageUploadStep/hooks/useProductImageOperations.ts @@ -1,4 +1,5 @@ import { toast } from "sonner"; +import { apiFetch } from '@/utils/api'; import config from "@/config"; import { Product, ProductImageSortable, ImageMetadata, ImageNotice } from "../types"; @@ -248,7 +249,7 @@ export const useProductImageOperations = ({ try { // Upload the image - const response = await fetch(`${config.apiUrl}/import/upload-image`, { + const response = await apiFetch(`${config.apiUrl}/import/upload-image`, { method: 'POST', body: formData, }); @@ -355,7 +356,7 @@ export const useProductImageOperations = ({ const filename = urlParts[urlParts.length - 1]; // Call API to delete the image - const response = await fetch(`${config.apiUrl}/import/delete-image`, { + const response = await apiFetch(`${config.apiUrl}/import/delete-image`, { method: 'DELETE', headers: { 'Content-Type': 'application/json', diff --git a/inventory/src/components/product-import/steps/ImageUploadStep/hooks/useUrlImageUpload.ts b/inventory/src/components/product-import/steps/ImageUploadStep/hooks/useUrlImageUpload.ts index 160d224..71f9236 100644 --- a/inventory/src/components/product-import/steps/ImageUploadStep/hooks/useUrlImageUpload.ts +++ b/inventory/src/components/product-import/steps/ImageUploadStep/hooks/useUrlImageUpload.ts @@ -1,4 +1,5 @@ import { useState } from "react"; +import { apiFetch } from '@/utils/api'; import { toast } from "sonner"; import config from "@/config"; import { Product, ProductImageSortable } from "../types"; @@ -59,7 +60,7 @@ export const useUrlImageUpload = ({ let fileName = "From URL"; if (isLikelyWebpUrl(validatedUrl)) { - const response = await fetch(`${config.apiUrl}/import/upload-image-url`, { + const response = await apiFetch(`${config.apiUrl}/import/upload-image-url`, { method: "POST", headers: { "Content-Type": "application/json", diff --git a/inventory/src/components/product-import/steps/MatchColumnsStep/MatchColumnsStep.tsx b/inventory/src/components/product-import/steps/MatchColumnsStep/MatchColumnsStep.tsx index 1d683a2..9e714e3 100644 --- a/inventory/src/components/product-import/steps/MatchColumnsStep/MatchColumnsStep.tsx +++ b/inventory/src/components/product-import/steps/MatchColumnsStep/MatchColumnsStep.tsx @@ -1,4 +1,5 @@ import React, { useCallback, useEffect, useMemo, useState, memo, useRef, useLayoutEffect } from "react" +import { apiFetch } from '@/utils/api'; import { useRsi } from "../../hooks/useRsi" import { setColumn } from "./utils/setColumn" import { setIgnoreColumn } from "./utils/setIgnoreColumn" @@ -672,7 +673,7 @@ const MatchColumnsStepComponent = ({ queryKey: ["product-lines", globalSelections.company], queryFn: async () => { if (!globalSelections.company) return []; - const response = await fetch(`${config.apiUrl}/import/product-lines/${globalSelections.company}`); + const response = await apiFetch(`${config.apiUrl}/import/product-lines/${globalSelections.company}`); if (!response.ok) { throw new Error("Failed to fetch product lines"); } @@ -687,7 +688,7 @@ const MatchColumnsStepComponent = ({ queryKey: ["sublines", globalSelections.line], queryFn: async () => { if (!globalSelections.line) return []; - const response = await fetch(`${config.apiUrl}/import/sublines/${globalSelections.line}`); + const response = await apiFetch(`${config.apiUrl}/import/sublines/${globalSelections.line}`); if (!response.ok) { throw new Error("Failed to fetch sublines"); } @@ -756,7 +757,7 @@ const MatchColumnsStepComponent = ({ queryKey: ["product-lines-mapped", mappedCompanyValue], queryFn: async () => { if (!mappedCompanyValue) return []; - const response = await fetch(`${config.apiUrl}/import/product-lines/${mappedCompanyValue}`); + const response = await apiFetch(`${config.apiUrl}/import/product-lines/${mappedCompanyValue}`); if (!response.ok) { throw new Error("Failed to fetch product lines for mapped company"); } @@ -771,7 +772,7 @@ const MatchColumnsStepComponent = ({ queryKey: ["sublines-mapped", mappedLineValue], queryFn: async () => { if (!mappedLineValue) return []; - const response = await fetch(`${config.apiUrl}/import/sublines/${mappedLineValue}`); + const response = await apiFetch(`${config.apiUrl}/import/sublines/${mappedLineValue}`); if (!response.ok) { throw new Error("Failed to fetch sublines for mapped line"); } @@ -785,7 +786,7 @@ const MatchColumnsStepComponent = ({ const { data: fieldOptionsData } = useQuery({ queryKey: ["field-options"], queryFn: async () => { - const response = await fetch(`${config.apiUrl}/import/field-options`); + const response = await apiFetch(`${config.apiUrl}/import/field-options`); if (!response.ok) { throw new Error("Failed to fetch field options"); } @@ -852,7 +853,7 @@ const MatchColumnsStepComponent = ({ setIsRefreshing(true); try { // Clear backend cache - const response = await fetch(`${config.apiUrl}/import/clear-taxonomy-cache`, { + const response = await apiFetch(`${config.apiUrl}/import/clear-taxonomy-cache`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/inventory/src/components/product-import/steps/ValidationStep/components/ValidationTable.tsx b/inventory/src/components/product-import/steps/ValidationStep/components/ValidationTable.tsx index 1006b1b..37d8d69 100644 --- a/inventory/src/components/product-import/steps/ValidationStep/components/ValidationTable.tsx +++ b/inventory/src/components/product-import/steps/ValidationStep/components/ValidationTable.tsx @@ -13,6 +13,7 @@ */ import { useMemo, useRef, useCallback, memo, useState } from 'react'; +import { apiFetch } from '@/utils/api'; import { type ColumnDef } from '@tanstack/react-table'; import { useVirtualizer } from '@tanstack/react-virtual'; import { Checkbox } from '@/components/ui/checkbox'; @@ -249,7 +250,7 @@ const CellWrapper = memo(({ setIsGeneratingUpc(true); try { - const response = await fetch(`${config.apiUrl}/import/generate-upc`, { + const response = await apiFetch(`${config.apiUrl}/import/generate-upc`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ supplierId: supplierIdString }), @@ -287,7 +288,7 @@ const CellWrapper = memo(({ startValidatingCell(rowIndex, 'item_number'); try { - const validationResponse = await fetch( + const validationResponse = await apiFetch( `${config.apiUrl}/import/check-upc-and-generate-sku?upc=${encodeURIComponent(upc)}&supplierId=${encodeURIComponent(supplierIdString)}` ); @@ -491,7 +492,7 @@ const CellWrapper = memo(({ if (!cached) { // Start loading state and fetch product lines state.setLoadingProductLines(companyId, true); - fetch(`/api/import/product-lines/${companyId}`) + apiFetch(`/api/import/product-lines/${companyId}`) .then(res => res.json()) .then(lines => { const opts = lines.map((line: { name?: string; label?: string; value?: string | number; id?: string | number }) => ({ @@ -523,7 +524,7 @@ const CellWrapper = memo(({ if (!cached) { // Start loading state and fetch sublines state.setLoadingSublines(lineId, true); - fetch(`/api/import/sublines/${lineId}`) + apiFetch(`/api/import/sublines/${lineId}`) .then(res => res.json()) .then(sublines => { const opts = sublines.map((subline: { name?: string; label?: string; value?: string | number; id?: string | number }) => ({ @@ -565,7 +566,7 @@ const CellWrapper = memo(({ line: valueToSave as string | number, }); - fetch('/api/ai/validate/inline/name', { + apiFetch('/api/ai/validate/inline/name', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ product: payload }), @@ -596,7 +597,7 @@ const CellWrapper = memo(({ const payload = buildDescriptionValidationPayload(currentRowForContext, fields); - fetch('/api/ai/validate/inline/description', { + apiFetch('/api/ai/validate/inline/description', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ product: payload }), @@ -640,7 +641,7 @@ const CellWrapper = memo(({ startValidatingCell(rowIndex, 'item_number'); try { - const response = await fetch( + const response = await apiFetch( `${config.apiUrl}/import/check-upc-and-generate-sku?upc=${encodeURIComponent(upc)}&supplierId=${encodeURIComponent(supplierId)}` ); @@ -726,7 +727,7 @@ const CellWrapper = memo(({ ? '/api/ai/validate/inline/name' : '/api/ai/validate/inline/description'; - fetch(endpoint, { + apiFetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ product: productPayload }), @@ -793,7 +794,7 @@ const CellWrapper = memo(({ ? '/api/ai/validate/inline/name' : '/api/ai/validate/inline/description'; - fetch(endpoint, { + apiFetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ product: payload }), @@ -824,7 +825,7 @@ const CellWrapper = memo(({ state.setLoadingProductLines(companyId, true); try { - const response = await fetch(`/api/import/product-lines/${companyId}`); + const response = await apiFetch(`/api/import/product-lines/${companyId}`); const lines = await response.json(); const opts = lines.map((ln: { name?: string; label?: string; value?: string | number; id?: string | number }) => ({ label: ln.name || ln.label || String(ln.value || ln.id), @@ -847,7 +848,7 @@ const CellWrapper = memo(({ state.setLoadingSublines(lineId, true); try { - const response = await fetch(`/api/import/sublines/${lineId}`); + const response = await apiFetch(`/api/import/sublines/${lineId}`); const sublines = await response.json(); const opts = sublines.map((subline: { name?: string; label?: string; value?: string | number; id?: string | number }) => ({ label: subline.name || subline.label || String(subline.value || subline.id), @@ -1138,7 +1139,7 @@ const TemplateCell = memo(({ rowIndex, currentTemplateId, defaultBrand }: Templa const payload = buildNameValidationPayload(updatedRow, fields, rows); - fetch('/api/ai/validate/inline/name', { + apiFetch('/api/ai/validate/inline/name', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ product: payload }), @@ -1164,7 +1165,7 @@ const TemplateCell = memo(({ rowIndex, currentTemplateId, defaultBrand }: Templa const payload = buildDescriptionValidationPayload(updatedRow, fields); - fetch('/api/ai/validate/inline/description', { + apiFetch('/api/ai/validate/inline/description', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ product: payload }), @@ -1206,7 +1207,7 @@ const TemplateCell = memo(({ rowIndex, currentTemplateId, defaultBrand }: Templa startValidatingCell(rowIndex, 'item_number'); try { - const response = await fetch( + const response = await apiFetch( `${config.apiUrl}/import/check-upc-and-generate-sku?upc=${encodeURIComponent(upc)}&supplierId=${encodeURIComponent(supplierId)}` ); diff --git a/inventory/src/components/product-import/steps/ValidationStep/contexts/AiSuggestionsContext.tsx b/inventory/src/components/product-import/steps/ValidationStep/contexts/AiSuggestionsContext.tsx index 25d57fb..019259c 100644 --- a/inventory/src/components/product-import/steps/ValidationStep/contexts/AiSuggestionsContext.tsx +++ b/inventory/src/components/product-import/steps/ValidationStep/contexts/AiSuggestionsContext.tsx @@ -11,6 +11,7 @@ */ import React, { createContext, useContext, useRef, useCallback, useEffect, useState } from 'react'; +import { apiFetch } from '@/utils/api'; import type { RowData, ProductSuggestions, TaxonomySuggestion } from '../store/types'; // ============================================================================ @@ -104,7 +105,7 @@ export function AiSuggestionsProvider({ if (isInitialized) return true; try { - const response = await fetch(`${API_BASE}/initialize`, { method: 'POST' }); + const response = await apiFetch(`${API_BASE}/initialize`, { method: 'POST' }); const data = await response.json(); if (!response.ok || !data.success) { @@ -157,7 +158,7 @@ export function AiSuggestionsProvider({ notifySubscribers(productIndex); try { - const response = await fetch(`${API_BASE}/suggestions`, { + const response = await apiFetch(`${API_BASE}/suggestions`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ product: productData }), @@ -214,7 +215,7 @@ export function AiSuggestionsProvider({ }); try { - const response = await fetch(`${API_BASE}/suggestions/batch`, { + const response = await apiFetch(`${API_BASE}/suggestions/batch`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ diff --git a/inventory/src/components/product-import/steps/ValidationStep/hooks/useAiValidation/useAiApi.ts b/inventory/src/components/product-import/steps/ValidationStep/hooks/useAiValidation/useAiApi.ts index 3922320..cb2f018 100644 --- a/inventory/src/components/product-import/steps/ValidationStep/hooks/useAiValidation/useAiApi.ts +++ b/inventory/src/components/product-import/steps/ValidationStep/hooks/useAiValidation/useAiApi.ts @@ -5,6 +5,7 @@ */ import config from '@/config'; +import { apiFetch } from '@/utils/api'; import type { RowData } from '../../store/types'; import type { Field } from '../../../../types'; import { prepareDataForAiValidation } from '../../utils/aiValidationUtils'; @@ -164,7 +165,7 @@ export const runAiValidation = async ( request: AiValidationRequest ): Promise => { try { - const response = await fetch(`${config.apiUrl}/ai-validation/validate`, { + const response = await apiFetch(`${config.apiUrl}/ai-validation/validate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(request), @@ -201,7 +202,7 @@ export const getAiDebugPrompt = async ( // For time estimation before validation, send all products const productsToSend = options?.previewOnly ? products.slice(0, 5) : products; - const response = await fetch(`${config.apiUrl}/ai-validation/debug`, { + const response = await apiFetch(`${config.apiUrl}/ai-validation/debug`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ diff --git a/inventory/src/components/product-import/steps/ValidationStep/hooks/useAutoInlineAiValidation.ts b/inventory/src/components/product-import/steps/ValidationStep/hooks/useAutoInlineAiValidation.ts index 263ccc4..cbfccc2 100644 --- a/inventory/src/components/product-import/steps/ValidationStep/hooks/useAutoInlineAiValidation.ts +++ b/inventory/src/components/product-import/steps/ValidationStep/hooks/useAutoInlineAiValidation.ts @@ -14,6 +14,7 @@ */ import { useEffect, useRef } from 'react'; +import { apiFetch } from '@/utils/api'; import { useValidationStore } from '../store/validationStore'; import { useInitPhase } from '../store/selectors'; import { @@ -48,7 +49,7 @@ async function triggerValidation( : '/api/ai/validate/inline/description'; try { - const response = await fetch(endpoint, { + const response = await apiFetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ product: payload }), diff --git a/inventory/src/components/product-import/steps/ValidationStep/hooks/useCopyDownValidation.ts b/inventory/src/components/product-import/steps/ValidationStep/hooks/useCopyDownValidation.ts index c7ba445..30d51a5 100644 --- a/inventory/src/components/product-import/steps/ValidationStep/hooks/useCopyDownValidation.ts +++ b/inventory/src/components/product-import/steps/ValidationStep/hooks/useCopyDownValidation.ts @@ -10,6 +10,7 @@ */ import { useEffect } from 'react'; +import { apiFetch } from '@/utils/api'; import { useValidationStore } from '../store/validationStore'; import { useUpcValidation } from './useUpcValidation'; import type { Field } from '../../../types'; @@ -47,7 +48,7 @@ async function triggerInlineAiValidation( : '/api/ai/validate/inline/description'; try { - const response = await fetch(endpoint, { + const response = await apiFetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ product: productPayload }), diff --git a/inventory/src/components/product-import/steps/ValidationStep/hooks/useFieldOptions.ts b/inventory/src/components/product-import/steps/ValidationStep/hooks/useFieldOptions.ts index 0008eed..bbe7779 100644 --- a/inventory/src/components/product-import/steps/ValidationStep/hooks/useFieldOptions.ts +++ b/inventory/src/components/product-import/steps/ValidationStep/hooks/useFieldOptions.ts @@ -6,6 +6,7 @@ */ import { useQuery } from '@tanstack/react-query'; +import { apiFetch } from '@/utils/api'; import config from '@/config'; import type { SelectOption } from '../../../types'; @@ -25,7 +26,7 @@ export interface FieldOptionsResponse { * Fetch field options from the API */ const fetchFieldOptions = async (): Promise => { - const response = await fetch(`${config.apiUrl}/import/field-options`); + const response = await apiFetch(`${config.apiUrl}/import/field-options`); if (!response.ok) { throw new Error('Failed to fetch field options'); } diff --git a/inventory/src/components/product-import/steps/ValidationStep/hooks/useInlineAiValidation.ts b/inventory/src/components/product-import/steps/ValidationStep/hooks/useInlineAiValidation.ts index 884f760..ca65e04 100644 --- a/inventory/src/components/product-import/steps/ValidationStep/hooks/useInlineAiValidation.ts +++ b/inventory/src/components/product-import/steps/ValidationStep/hooks/useInlineAiValidation.ts @@ -6,6 +6,7 @@ */ import { useState, useCallback, useRef } from 'react'; +import { apiFetch } from '@/utils/api'; // Types for the validation results export interface InlineAiResult { @@ -73,7 +74,7 @@ export function useInlineAiValidation() { setState(prev => ({ ...prev, isValidating: true, error: null })); try { - const response = await fetch('/api/ai/validate/inline/name', { + const response = await apiFetch('/api/ai/validate/inline/name', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ product }), @@ -138,7 +139,7 @@ export function useInlineAiValidation() { setState(prev => ({ ...prev, isValidating: true, error: null })); try { - const response = await fetch('/api/ai/validate/inline/description', { + const response = await apiFetch('/api/ai/validate/inline/description', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ product }), diff --git a/inventory/src/components/product-import/steps/ValidationStep/hooks/useProductLines.ts b/inventory/src/components/product-import/steps/ValidationStep/hooks/useProductLines.ts index 60561d3..576f45a 100644 --- a/inventory/src/components/product-import/steps/ValidationStep/hooks/useProductLines.ts +++ b/inventory/src/components/product-import/steps/ValidationStep/hooks/useProductLines.ts @@ -12,13 +12,13 @@ import { useCallback, useRef } from 'react'; import { useValidationStore } from '../store/validationStore'; import type { SelectOption } from '../../../types'; -import axios from 'axios'; +import { apiClient } from "@/utils/apiClient"; /** * Fetch product lines for a company */ const fetchProductLinesApi = async (companyId: string): Promise => { - const response = await axios.get(`/api/import/product-lines/${companyId}`); + const response = await apiClient.get(`/api/import/product-lines/${companyId}`); const lines = response.data; return lines.map((line: { name?: string; label?: string; value?: string | number; id?: string | number }) => ({ @@ -31,7 +31,7 @@ const fetchProductLinesApi = async (companyId: string): Promise * Fetch sublines for a product line */ const fetchSublinesApi = async (lineId: string): Promise => { - const response = await axios.get(`/api/import/sublines/${lineId}`); + const response = await apiClient.get(`/api/import/sublines/${lineId}`); const sublines = response.data; return sublines.map((subline: { name?: string; label?: string; value?: string | number; id?: string | number }) => ({ diff --git a/inventory/src/components/product-import/steps/ValidationStep/hooks/useSanityCheck.ts b/inventory/src/components/product-import/steps/ValidationStep/hooks/useSanityCheck.ts index a176de4..bfd0e32 100644 --- a/inventory/src/components/product-import/steps/ValidationStep/hooks/useSanityCheck.ts +++ b/inventory/src/components/product-import/steps/ValidationStep/hooks/useSanityCheck.ts @@ -9,6 +9,7 @@ */ import { useState, useCallback, useRef } from 'react'; +import { apiFetch } from '@/utils/api'; // Types for sanity check results export interface SanityIssue { @@ -103,7 +104,7 @@ export function useSanityCheck() { })); try { - const response = await fetch('/api/ai/validate/sanity-check', { + const response = await apiFetch('/api/ai/validate/sanity-check', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ products }), diff --git a/inventory/src/components/product-import/steps/ValidationStep/hooks/useTemplateManagement.ts b/inventory/src/components/product-import/steps/ValidationStep/hooks/useTemplateManagement.ts index 1c7df9b..123d2b2 100644 --- a/inventory/src/components/product-import/steps/ValidationStep/hooks/useTemplateManagement.ts +++ b/inventory/src/components/product-import/steps/ValidationStep/hooks/useTemplateManagement.ts @@ -10,6 +10,7 @@ */ import { useCallback } from 'react'; +import { apiFetch } from '@/utils/api'; import { useValidationStore } from '../store/validationStore'; import { useTemplates, @@ -56,7 +57,7 @@ export const useTemplateManagement = () => { setTemplatesLoading(true); try { - const response = await fetch(`${config.apiUrl}/templates`); + const response = await apiFetch(`${config.apiUrl}/templates`); if (!response.ok) throw new Error('Failed to fetch templates'); const data = await response.json(); @@ -162,7 +163,7 @@ export const useTemplateManagement = () => { if (supplierId && upcValue) { // Don't await - let it run in background // UPC validation uses its own hooks which will update the store - fetch(`/api/import/validate-upc/${supplierId}/${encodeURIComponent(upcValue)}`) + apiFetch(`/api/import/validate-upc/${supplierId}/${encodeURIComponent(upcValue)}`) .then((res) => res.json()) .then((result) => { if (result.itemNumber) { @@ -236,7 +237,7 @@ export const useTemplateManagement = () => { } try { - const response = await fetch(`${config.apiUrl}/templates`, { + const response = await apiFetch(`${config.apiUrl}/templates`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ diff --git a/inventory/src/components/product-import/steps/ValidationStep/hooks/useUpcValidation.ts b/inventory/src/components/product-import/steps/ValidationStep/hooks/useUpcValidation.ts index 814ddb1..ca36c6a 100644 --- a/inventory/src/components/product-import/steps/ValidationStep/hooks/useUpcValidation.ts +++ b/inventory/src/components/product-import/steps/ValidationStep/hooks/useUpcValidation.ts @@ -10,6 +10,7 @@ */ import { useCallback, useRef } from 'react'; +import { apiFetch } from '@/utils/api'; import { useValidationStore } from '../store/validationStore'; import { useInitialUpcValidationDone } from '../store/selectors'; import { ErrorSource, ErrorType, type UpcValidationResult } from '../store/types'; @@ -27,7 +28,7 @@ const fetchProductByUpc = async ( upcValue: string ): Promise => { try { - const response = await fetch( + const response = await apiFetch( `${config.apiUrl}/import/check-upc-and-generate-sku?upc=${encodeURIComponent(upcValue)}&supplierId=${encodeURIComponent(supplierId)}` ); diff --git a/inventory/src/components/product-import/steps/ValidationStep/index.tsx b/inventory/src/components/product-import/steps/ValidationStep/index.tsx index c399fe0..457ea7b 100644 --- a/inventory/src/components/product-import/steps/ValidationStep/index.tsx +++ b/inventory/src/components/product-import/steps/ValidationStep/index.tsx @@ -9,6 +9,7 @@ */ import { useEffect, useRef, useDeferredValue, useMemo } from 'react'; +import { apiFetch } from '@/utils/api'; import { useQuery } from '@tanstack/react-query'; import { useValidationStore } from './store/validationStore'; import { useInitPhase, useIsReady } from './store/selectors'; @@ -58,7 +59,7 @@ const createDataFingerprint = (data: Record[]): string => { * Fetch field options from the API */ const fetchFieldOptions = async () => { - const response = await fetch(`${config.apiUrl}/import/field-options`); + const response = await apiFetch(`${config.apiUrl}/import/field-options`); if (!response.ok) { throw new Error('Failed to fetch field options'); } diff --git a/inventory/src/components/products/ProductDetail.tsx b/inventory/src/components/products/ProductDetail.tsx index c9d0057..a539a1f 100644 --- a/inventory/src/components/products/ProductDetail.tsx +++ b/inventory/src/components/products/ProductDetail.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { apiFetch } from '@/utils/api'; import { useQuery } from "@tanstack/react-query"; import { Drawer as VaulDrawer } from "vaul"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; @@ -77,7 +78,7 @@ export function ProductDetail({ productId, onClose }: ProductDetailProps) { queryKey: ["productMetricDetail", productId], queryFn: async () => { if (!productId) throw new Error("Product ID is required"); - const response = await fetch(`${config.apiUrl}/metrics/${productId}`, {credentials: 'include'}); + const response = await apiFetch(`${config.apiUrl}/metrics/${productId}`, {credentials: 'include'}); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(`Failed to fetch product details (${response.status}): ${errorData.error || 'Server error'}`); @@ -93,7 +94,7 @@ export function ProductDetail({ productId, onClose }: ProductDetailProps) { queryKey: ["productTimeSeries", productId], queryFn: async () => { if (!productId) throw new Error("Product ID is required"); - const response = await fetch(`${config.apiUrl}/products/${productId}/time-series`, {credentials: 'include'}); + const response = await apiFetch(`${config.apiUrl}/products/${productId}/time-series`, {credentials: 'include'}); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(`Failed to fetch time series data (${response.status}): ${errorData.error || 'Server error'}`); @@ -135,7 +136,7 @@ export function ProductDetail({ productId, onClose }: ProductDetailProps) { queryKey: ["productForecast", productId], queryFn: async () => { if (!productId) throw new Error("Product ID is required"); - const response = await fetch(`${config.apiUrl}/products/${productId}/forecast`, {credentials: 'include'}); + const response = await apiFetch(`${config.apiUrl}/products/${productId}/forecast`, {credentials: 'include'}); if (!response.ok) throw new Error("Failed to fetch forecast"); return response.json(); }, @@ -682,4 +683,4 @@ const InfoItem = ({ label, value, isLarge = false }: { label: string; value: Rea
{label}
{value}
-); \ No newline at end of file +); diff --git a/inventory/src/components/products/ProductSummaryCards.tsx b/inventory/src/components/products/ProductSummaryCards.tsx index f413229..c3e4c41 100644 --- a/inventory/src/components/products/ProductSummaryCards.tsx +++ b/inventory/src/components/products/ProductSummaryCards.tsx @@ -1,4 +1,5 @@ import { useQuery } from '@tanstack/react-query'; +import { apiFetch } from '@/utils/api'; import { DollarSign, Package, @@ -282,7 +283,7 @@ export function ProductSummaryCards({ activeView, showNonReplenishable, showInvi const params = new URLSearchParams(); if (showNonReplenishable) params.append('showNonReplenishable', 'true'); if (showInvisible) params.append('showInvisible', 'true'); - const response = await fetch(`/api/metrics/summary?${params.toString()}`, { credentials: 'include' }); + const response = await apiFetch(`/api/metrics/summary?${params.toString()}`, { credentials: 'include' }); if (!response.ok) throw new Error('Failed to fetch summary'); return response.json(); }, diff --git a/inventory/src/components/purchase-orders/PipelineCard.tsx b/inventory/src/components/purchase-orders/PipelineCard.tsx index b73cde7..c0840b6 100644 --- a/inventory/src/components/purchase-orders/PipelineCard.tsx +++ b/inventory/src/components/purchase-orders/PipelineCard.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react'; +import { apiFetch } from '@/utils/api'; import { ResponsiveContainer, BarChart, @@ -40,7 +41,7 @@ export default function PipelineCard() { const [loading, setLoading] = useState(true); useEffect(() => { - fetch('/api/purchase-orders/pipeline') + apiFetch('/api/purchase-orders/pipeline') .then(res => res.ok ? res.json() : Promise.reject('Failed')) .then(setData) .catch(err => console.error('Pipeline fetch error:', err)) diff --git a/inventory/src/components/purchase-orders/PurchaseOrderAccordion.tsx b/inventory/src/components/purchase-orders/PurchaseOrderAccordion.tsx index e5ba4f3..7945095 100644 --- a/inventory/src/components/purchase-orders/PurchaseOrderAccordion.tsx +++ b/inventory/src/components/purchase-orders/PurchaseOrderAccordion.tsx @@ -1,4 +1,5 @@ import React, { useState, useEffect } from "react"; +import { apiFetch } from '@/utils/api'; import { Table, TableBody, @@ -85,7 +86,7 @@ export default function PurchaseOrderAccordion({ ? `/api/purchase-orders/receiving/${purchaseOrder.id}/items` : `/api/purchase-orders/${purchaseOrder.id}/items`; - const response = await fetch(endpoint); + const response = await apiFetch(endpoint); if (!response.ok) { throw new Error(`Failed to fetch items: ${response.statusText}`); @@ -238,4 +239,4 @@ export default function PurchaseOrderAccordion({ )} ); -} \ No newline at end of file +} diff --git a/inventory/src/components/settings/AuditLog.tsx b/inventory/src/components/settings/AuditLog.tsx index a892614..93162b2 100644 --- a/inventory/src/components/settings/AuditLog.tsx +++ b/inventory/src/components/settings/AuditLog.tsx @@ -1,5 +1,5 @@ import { useState, useEffect, useCallback } from "react"; -import axios from "axios"; +import { apiClient } from "@/utils/apiClient"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -92,7 +92,7 @@ export function AuditLog() { if (filterPid.trim()) params.pid = filterPid.trim(); if (filterAction !== "all") params.action = filterAction; } - const res = await axios.get(baseUrl, { params }); + const res = await apiClient.get(baseUrl, { params }); setEntries(res.data.entries); setTotal(res.data.total); } catch { @@ -125,7 +125,7 @@ export function AuditLog() { const viewDetail = async (id: number) => { setIsLoadingDetail(true); try { - const res = await axios.get(`${baseUrl}/${id}`); + const res = await apiClient.get(`${baseUrl}/${id}`); setDetail(res.data); } catch { setDetail(null); diff --git a/inventory/src/components/settings/DataManagement.tsx b/inventory/src/components/settings/DataManagement.tsx index 9a21fc2..e9ec1d3 100644 --- a/inventory/src/components/settings/DataManagement.tsx +++ b/inventory/src/components/settings/DataManagement.tsx @@ -1,4 +1,5 @@ import { useState, useEffect, useRef } from "react"; +import { apiFetch } from '@/utils/api'; import { Button } from "@/components/ui/button"; import { Card, @@ -189,7 +190,7 @@ export function DataManagement() { const checkActiveProcess = async () => { try { console.log("Checking for active processes..."); - const response = await fetch(`${config.apiUrl}/csv/status`, { + const response = await apiFetch(`${config.apiUrl}/csv/status`, { credentials: "include", }); @@ -509,7 +510,7 @@ export function DataManagement() { // Connect to the update SSE endpoint connectToEventSource("update"); - const response = await fetch(`${config.apiUrl}/csv/full-update`, { + const response = await apiFetch(`${config.apiUrl}/csv/full-update`, { method: "POST", credentials: "include", }); @@ -543,7 +544,7 @@ export function DataManagement() { // Connect to the reset SSE endpoint connectToEventSource("reset"); - const response = await fetch(`${config.apiUrl}/csv/full-reset`, { + const response = await apiFetch(`${config.apiUrl}/csv/full-reset`, { method: "POST", credentials: "include", }); @@ -572,7 +573,7 @@ export function DataManagement() { // Determine which operation is running const type = isUpdating ? "update" : "reset"; - const response = await fetch(`${config.apiUrl}/csv/cancel?type=${type}`, { + const response = await apiFetch(`${config.apiUrl}/csv/cancel?type=${type}`, { method: "POST", credentials: "include", }); @@ -613,11 +614,11 @@ export function DataManagement() { console.log("Fetching history data..."); const [importRes, calcRes, moduleRes, tableRes, tableCountsRes] = await Promise.all([ - fetch(`${config.apiUrl}/csv/history/import`, { credentials: 'include' }), - fetch(`${config.apiUrl}/csv/history/calculate`, { credentials: 'include' }), - fetch(`${config.apiUrl}/csv/status/modules`, { credentials: 'include' }), - fetch(`${config.apiUrl}/csv/status/tables`, { credentials: 'include' }), - fetch(`${config.apiUrl}/csv/status/table-counts`, { credentials: 'include' }), + apiFetch(`${config.apiUrl}/csv/history/import`, { credentials: 'include' }), + apiFetch(`${config.apiUrl}/csv/history/calculate`, { credentials: 'include' }), + apiFetch(`${config.apiUrl}/csv/status/modules`, { credentials: 'include' }), + apiFetch(`${config.apiUrl}/csv/status/tables`, { credentials: 'include' }), + apiFetch(`${config.apiUrl}/csv/status/table-counts`, { credentials: 'include' }), ]); console.log("Fetch responses:", { diff --git a/inventory/src/components/settings/GlobalSettings.tsx b/inventory/src/components/settings/GlobalSettings.tsx index 255a326..1dc86fc 100644 --- a/inventory/src/components/settings/GlobalSettings.tsx +++ b/inventory/src/components/settings/GlobalSettings.tsx @@ -1,4 +1,5 @@ import { useState, useEffect } from 'react'; +import { apiFetch } from '@/utils/api'; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; @@ -25,7 +26,7 @@ export function GlobalSettings() { const loadSettings = async () => { try { setLoading(true); - const response = await fetch(`${config.apiUrl}/config/global`, { + const response = await apiFetch(`${config.apiUrl}/config/global`, { credentials: 'include' }); if (!response.ok) { @@ -49,7 +50,7 @@ export function GlobalSettings() { const handleSaveSettings = async () => { try { - const response = await fetch(`${config.apiUrl}/config/global`, { + const response = await apiFetch(`${config.apiUrl}/config/global`, { method: 'PUT', headers: { 'Content-Type': 'application/json' @@ -185,4 +186,4 @@ export function GlobalSettings() { ); -} \ No newline at end of file +} diff --git a/inventory/src/components/settings/ProductSettings.tsx b/inventory/src/components/settings/ProductSettings.tsx index b108bc4..5a8587e 100644 --- a/inventory/src/components/settings/ProductSettings.tsx +++ b/inventory/src/components/settings/ProductSettings.tsx @@ -1,4 +1,5 @@ import { useState, useEffect, useCallback, useMemo } from 'react'; +import { apiFetch } from '@/utils/api'; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; @@ -43,7 +44,7 @@ export function ProductSettings() { const loadSettings = useCallback(async () => { try { setLoading(true); - const response = await fetch(`${config.apiUrl}/config/products?page=${page}&pageSize=${pageSize}&search=${encodeURIComponent(searchQuery)}`, { + const response = await apiFetch(`${config.apiUrl}/config/products?page=${page}&pageSize=${pageSize}&search=${encodeURIComponent(searchQuery)}`, { credentials: 'include' }); if (!response.ok) { @@ -75,7 +76,7 @@ export function ProductSettings() { const setting = settings.find(s => s.pid === pid); if (!setting) return; - const response = await fetch(`${config.apiUrl}/config/products/${pid}`, { + const response = await apiFetch(`${config.apiUrl}/config/products/${pid}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' @@ -104,7 +105,7 @@ export function ProductSettings() { const handleResetToDefault = useCallback(async (pid: string) => { try { - const response = await fetch(`${config.apiUrl}/config/products/${pid}/reset`, { + const response = await apiFetch(`${config.apiUrl}/config/products/${pid}/reset`, { method: 'POST', credentials: 'include' }); @@ -319,4 +320,4 @@ export function ProductSettings() { ); -} \ No newline at end of file +} diff --git a/inventory/src/components/settings/PromptManagement.tsx b/inventory/src/components/settings/PromptManagement.tsx index 196f33a..a2827a0 100644 --- a/inventory/src/components/settings/PromptManagement.tsx +++ b/inventory/src/components/settings/PromptManagement.tsx @@ -1,4 +1,5 @@ import { useState, useMemo } from "react"; +import { apiFetch } from '@/utils/api'; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { Button } from "@/components/ui/button"; import { @@ -148,7 +149,7 @@ export function PromptManagement() { const { data: prompts, isLoading } = useQuery({ queryKey: ["ai-prompts"], queryFn: async () => { - const response = await fetch(`${config.apiUrl}/ai-prompts`); + const response = await apiFetch(`${config.apiUrl}/ai-prompts`); if (!response.ok) { throw new Error("Failed to fetch AI prompts"); } @@ -159,7 +160,7 @@ export function PromptManagement() { const { data: fieldOptions } = useQuery({ queryKey: ["fieldOptions"], queryFn: async () => { - const response = await fetch(`${config.apiUrl}/import/field-options`); + const response = await apiFetch(`${config.apiUrl}/import/field-options`); if (!response.ok) { throw new Error("Failed to fetch field options"); } @@ -194,7 +195,7 @@ export function PromptManagement() { const createMutation = useMutation({ mutationFn: async (data: { prompt_text: string; prompt_type: string; company: string | null; is_singleton: boolean }) => { - const response = await fetch(`${config.apiUrl}/ai-prompts`, { + const response = await apiFetch(`${config.apiUrl}/ai-prompts`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), @@ -220,7 +221,7 @@ export function PromptManagement() { const updateMutation = useMutation({ mutationFn: async (data: { id: number; prompt_text: string; prompt_type: string; company: string | null; is_singleton: boolean }) => { - const response = await fetch(`${config.apiUrl}/ai-prompts/${data.id}`, { + const response = await apiFetch(`${config.apiUrl}/ai-prompts/${data.id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), @@ -248,7 +249,7 @@ export function PromptManagement() { const deleteMutation = useMutation({ mutationFn: async (id: number) => { - const response = await fetch(`${config.apiUrl}/ai-prompts/${id}`, { + const response = await apiFetch(`${config.apiUrl}/ai-prompts/${id}`, { method: "DELETE", }); if (!response.ok) { diff --git a/inventory/src/components/settings/ReusableImageManagement.tsx b/inventory/src/components/settings/ReusableImageManagement.tsx index 9f4b425..f787647 100644 --- a/inventory/src/components/settings/ReusableImageManagement.tsx +++ b/inventory/src/components/settings/ReusableImageManagement.tsx @@ -1,4 +1,5 @@ import { useState, useMemo, useCallback, useEffect } from "react"; +import { apiFetch } from '@/utils/api'; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { Button } from "@/components/ui/button"; import { @@ -235,7 +236,7 @@ export function ReusableImageManagement() { const { data: images, isLoading } = useQuery({ queryKey: ["reusable-images"], queryFn: async () => { - const response = await fetch(`${config.apiUrl}/reusable-images`); + const response = await apiFetch(`${config.apiUrl}/reusable-images`); if (!response.ok) { throw new Error("Failed to fetch reusable images"); } @@ -246,7 +247,7 @@ export function ReusableImageManagement() { const { data: fieldOptions } = useQuery({ queryKey: ["fieldOptions"], queryFn: async () => { - const response = await fetch(`${config.apiUrl}/import/field-options`); + const response = await apiFetch(`${config.apiUrl}/import/field-options`); if (!response.ok) { throw new Error("Failed to fetch field options"); } @@ -271,7 +272,7 @@ export function ReusableImageManagement() { throw new Error("Image file is required"); } - const response = await fetch(`${config.apiUrl}/reusable-images/upload`, { + const response = await apiFetch(`${config.apiUrl}/reusable-images/upload`, { method: "POST", body: formData, }); @@ -297,7 +298,7 @@ export function ReusableImageManagement() { mutationFn: async (data: ImageFormData) => { if (!data.id) throw new Error("Image ID is required for update"); - const response = await fetch(`${config.apiUrl}/reusable-images/${data.id}`, { + const response = await apiFetch(`${config.apiUrl}/reusable-images/${data.id}`, { method: "PUT", headers: { "Content-Type": "application/json", @@ -328,7 +329,7 @@ export function ReusableImageManagement() { const deleteMutation = useMutation({ mutationFn: async (id: number) => { - const response = await fetch(`${config.apiUrl}/reusable-images/${id}`, { + const response = await apiFetch(`${config.apiUrl}/reusable-images/${id}`, { method: "DELETE", }); if (!response.ok) { @@ -776,4 +777,4 @@ const ImagePreview = ({ file }: { file: File }) => { className="max-h-32 max-w-full object-contain rounded-md" /> ); -}; \ No newline at end of file +}; diff --git a/inventory/src/components/settings/TemplateManagement.tsx b/inventory/src/components/settings/TemplateManagement.tsx index cd9ea4d..deae962 100644 --- a/inventory/src/components/settings/TemplateManagement.tsx +++ b/inventory/src/components/settings/TemplateManagement.tsx @@ -1,4 +1,5 @@ import { useState, useMemo } from "react"; +import { apiFetch } from '@/utils/api'; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { Button } from "@/components/ui/button"; import { TemplateForm } from "@/components/templates/TemplateForm"; @@ -91,7 +92,7 @@ export function TemplateManagement() { const { data: templates, isLoading } = useQuery({ queryKey: ["templates"], queryFn: async () => { - const response = await fetch(`${config.apiUrl}/templates`); + const response = await apiFetch(`${config.apiUrl}/templates`); if (!response.ok) { throw new Error("Failed to fetch templates"); } @@ -102,7 +103,7 @@ export function TemplateManagement() { const { data: fieldOptions } = useQuery({ queryKey: ["fieldOptions"], queryFn: async () => { - const response = await fetch(`${config.apiUrl}/import/field-options`); + const response = await apiFetch(`${config.apiUrl}/import/field-options`); if (!response.ok) { throw new Error("Failed to fetch field options"); } @@ -112,7 +113,7 @@ export function TemplateManagement() { const deleteMutation = useMutation({ mutationFn: async (id: number) => { - const response = await fetch(`${config.apiUrl}/templates/${id}`, { + const response = await apiFetch(`${config.apiUrl}/templates/${id}`, { method: "DELETE", }); if (!response.ok) { @@ -368,4 +369,4 @@ export function TemplateManagement() { ); -} \ No newline at end of file +} diff --git a/inventory/src/components/settings/UserForm.tsx b/inventory/src/components/settings/UserForm.tsx index 4e1de0c..4ac8009 100644 --- a/inventory/src/components/settings/UserForm.tsx +++ b/inventory/src/components/settings/UserForm.tsx @@ -1,4 +1,5 @@ import { useState, useEffect } from "react"; +import { apiFetch } from '@/utils/api'; import { z } from "zod"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; @@ -122,7 +123,7 @@ export function UserForm({ user, permissions, onSave, onCancel }: UserFormProps) useEffect(() => { const fetchRocketChatUsers = async () => { try { - const response = await fetch(`${config.chatUrl}/users`); + const response = await apiFetch(`${config.chatUrl}/users`); const data = await response.json(); if (data.status === 'success') { @@ -431,4 +432,4 @@ export function UserForm({ user, permissions, onSave, onCancel }: UserFormProps) ); -} \ No newline at end of file +} diff --git a/inventory/src/components/settings/UserManagement.tsx b/inventory/src/components/settings/UserManagement.tsx index ce1e867..b6084ad 100644 --- a/inventory/src/components/settings/UserManagement.tsx +++ b/inventory/src/components/settings/UserManagement.tsx @@ -1,4 +1,5 @@ import { useState, useEffect, useContext } from "react"; +import { apiFetch } from '@/utils/api'; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; @@ -56,7 +57,7 @@ export function UserManagement() { setError(null); // Fetch users - const usersResponse = await fetch(`${config.authUrl}/users`, { + const usersResponse = await apiFetch(`${config.authUrl}/users`, { headers: { 'Authorization': `Bearer ${token}` } @@ -82,7 +83,7 @@ export function UserManagement() { setUsers(usersData); // Fetch permissions - const permissionsResponse = await fetch(`${config.authUrl}/permissions/categories`, { + const permissionsResponse = await apiFetch(`${config.authUrl}/permissions/categories`, { headers: { 'Authorization': `Bearer ${token}` } @@ -128,7 +129,7 @@ export function UserManagement() { const handleEditUser = async (userId: number) => { try { - const response = await fetch(`${config.authUrl}/users/${userId}`, { + const response = await apiFetch(`${config.authUrl}/users/${userId}`, { headers: { 'Authorization': `Bearer ${token}` } @@ -216,7 +217,7 @@ export function UserManagement() { console.log(`${method} request to ${endpoint}`); - const response = await fetch(endpoint, { + const response = await apiFetch(endpoint, { method, headers: { 'Content-Type': 'application/json', @@ -264,7 +265,7 @@ export function UserManagement() { try { setLoading(true); - const response = await fetch(`${config.authUrl}/users/${userId}`, { + const response = await apiFetch(`${config.authUrl}/users/${userId}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}` @@ -350,4 +351,4 @@ export function UserManagement() { )} ); -} \ No newline at end of file +} diff --git a/inventory/src/components/settings/VendorSettings.tsx b/inventory/src/components/settings/VendorSettings.tsx index 933fa98..713952c 100644 --- a/inventory/src/components/settings/VendorSettings.tsx +++ b/inventory/src/components/settings/VendorSettings.tsx @@ -1,4 +1,5 @@ import { useState, useEffect, useCallback, useMemo } from 'react'; +import { apiFetch } from '@/utils/api'; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; @@ -39,7 +40,7 @@ export function VendorSettings() { const loadSettings = useCallback(async () => { try { setLoading(true); - const response = await fetch(`${config.apiUrl}/config/vendors?page=${page}&pageSize=${pageSize}&search=${encodeURIComponent(searchQuery)}`, { + const response = await apiFetch(`${config.apiUrl}/config/vendors?page=${page}&pageSize=${pageSize}&search=${encodeURIComponent(searchQuery)}`, { credentials: 'include' }); if (!response.ok) { @@ -75,7 +76,7 @@ export function VendorSettings() { const setting = settings.find(s => s.vendor === vendor); if (!setting) return; - const response = await fetch(`${config.apiUrl}/config/vendors/${encodeURIComponent(vendor)}`, { + const response = await apiFetch(`${config.apiUrl}/config/vendors/${encodeURIComponent(vendor)}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' @@ -108,7 +109,7 @@ export function VendorSettings() { const handleResetToDefault = useCallback(async (vendor: string) => { try { - const response = await fetch(`${config.apiUrl}/config/vendors/${encodeURIComponent(vendor)}/reset`, { + const response = await apiFetch(`${config.apiUrl}/config/vendors/${encodeURIComponent(vendor)}/reset`, { method: 'POST', credentials: 'include' }); @@ -297,4 +298,4 @@ export function VendorSettings() { ); -} \ No newline at end of file +} diff --git a/inventory/src/components/templates/SearchProductTemplateDialog.tsx b/inventory/src/components/templates/SearchProductTemplateDialog.tsx index 8d8f850..9951fe7 100644 --- a/inventory/src/components/templates/SearchProductTemplateDialog.tsx +++ b/inventory/src/components/templates/SearchProductTemplateDialog.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import axios from 'axios'; +import { apiClient } from "@/utils/apiClient"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; @@ -342,7 +342,7 @@ export function SearchProductTemplateDialog({ isOpen, onClose, onTemplateCreated apiParams.q = '*'; } - const response = await axios.get('/api/import/search-products', { params: apiParams }); + const response = await apiClient.get('/api/import/search-products', { params: apiParams }); setSearchResults(response.data.map((product: Product) => ({ ...product, @@ -408,7 +408,7 @@ export function SearchProductTemplateDialog({ isOpen, onClose, onTemplateCreated const fetchFieldOptions = async () => { try { - const response = await axios.get('/api/import/field-options'); + const response = await apiClient.get('/api/import/field-options'); setFieldOptions(response.data); } catch (error) { console.error('Error fetching field options:', error); @@ -442,7 +442,7 @@ export function SearchProductTemplateDialog({ isOpen, onClose, onTemplateCreated // Fetch product categories if pid is available if (product.pid) { try { - const response = await axios.get(`/api/import/product-categories/${product.pid}`); + const response = await apiClient.get(`/api/import/product-categories/${product.pid}`); const productCategories = response.data; // Update the selected product with the categories diff --git a/inventory/src/components/templates/TemplateForm.tsx b/inventory/src/components/templates/TemplateForm.tsx index eab5d24..8d8f576 100644 --- a/inventory/src/components/templates/TemplateForm.tsx +++ b/inventory/src/components/templates/TemplateForm.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; -import axios from 'axios'; +import { apiClient } from "@/utils/apiClient"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from '@/components/ui/dialog'; import { Textarea } from '@/components/ui/textarea'; import { Loader2, ChevronsUpDown, Check } from 'lucide-react'; @@ -230,7 +230,7 @@ export function TemplateForm({ console.log('Sending request to:', endpoint, 'with data:', cleanFormData); - await axios[method](endpoint, cleanFormData); + await apiClient[method](endpoint, cleanFormData); toast.success( mode === 'edit' ? 'Template updated successfully' : 'Template created successfully' diff --git a/inventory/src/pages/Analytics.tsx b/inventory/src/pages/Analytics.tsx index d7301f8..9c57292 100644 --- a/inventory/src/pages/Analytics.tsx +++ b/inventory/src/pages/Analytics.tsx @@ -1,4 +1,5 @@ import { useQuery } from '@tanstack/react-query'; +import { apiFetch } from '@/utils/api'; import { Card, CardContent, CardHeader, CardTitle } from '../components/ui/card'; import { InventoryValueTrend } from '../components/analytics/InventoryValueTrend'; import { InventoryFlow } from '../components/analytics/InventoryFlow'; @@ -31,7 +32,7 @@ export function Analytics() { const { data: summary, isLoading, isError } = useQuery({ queryKey: ['inventory-summary'], queryFn: async () => { - const response = await fetch(`${config.apiUrl}/analytics/inventory-summary`); + const response = await apiFetch(`${config.apiUrl}/analytics/inventory-summary`); if (!response.ok) throw new Error('Failed to fetch inventory summary'); return response.json(); }, diff --git a/inventory/src/pages/BlackFridayDashboard.tsx b/inventory/src/pages/BlackFridayDashboard.tsx index 61cc9cb..ad8911e 100644 --- a/inventory/src/pages/BlackFridayDashboard.tsx +++ b/inventory/src/pages/BlackFridayDashboard.tsx @@ -1,4 +1,5 @@ import { useEffect, useMemo, useState } from "react"; +import { apiFetch } from '@/utils/api'; import { Card, CardContent, @@ -655,7 +656,7 @@ export function BlackFridayDashboard() { const fetchRealtime = async () => { try { - const response = await fetch("/api/dashboard-analytics/realtime/basic", { + const response = await apiFetch("/api/dashboard-analytics/realtime/basic", { credentials: "include", }); diff --git a/inventory/src/pages/Brands.tsx b/inventory/src/pages/Brands.tsx index 96f704e..34afc95 100644 --- a/inventory/src/pages/Brands.tsx +++ b/inventory/src/pages/Brands.tsx @@ -1,4 +1,5 @@ import { useState, useMemo, useCallback, type ReactNode } from "react"; +import { apiFetch } from '@/utils/api'; import { useQuery } from "@tanstack/react-query"; import { motion } from "framer-motion"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; @@ -231,7 +232,7 @@ function useAggregateList(opts: UseAggregateLis const query = useQuery({ queryKey: [opts.queryKeyPrefix, queryParams.toString()], queryFn: async () => { - const response = await fetch(`${config.apiUrl}/${opts.endpoint}?${queryParams.toString()}`, { + const response = await apiFetch(`${config.apiUrl}/${opts.endpoint}?${queryParams.toString()}`, { credentials: "include", }); if (!response.ok) throw new Error(`Network response was not ok (${response.status})`); @@ -420,7 +421,7 @@ function BrandsPanel() { const { data: statsData, isLoading: isLoadingStats } = useQuery({ queryKey: ["brandsStats"], queryFn: async () => { - const response = await fetch(`${config.apiUrl}/brands-aggregate/stats`, { credentials: "include" }); + const response = await apiFetch(`${config.apiUrl}/brands-aggregate/stats`, { credentials: "include" }); if (!response.ok) throw new Error("Failed to fetch brand stats"); return response.json(); }, @@ -429,7 +430,7 @@ function BrandsPanel() { const { data: filterOptions } = useQuery({ queryKey: ["brandsFilterOptions"], queryFn: async () => { - const response = await fetch(`${config.apiUrl}/brands-aggregate/filter-options`, { credentials: "include" }); + const response = await apiFetch(`${config.apiUrl}/brands-aggregate/filter-options`, { credentials: "include" }); if (!response.ok) throw new Error("Failed to fetch filter options"); return response.json(); }, @@ -619,7 +620,7 @@ function VendorsPanel() { const { data: statsData, isLoading: isLoadingStats } = useQuery({ queryKey: ["vendorsStats"], queryFn: async () => { - const response = await fetch(`${config.apiUrl}/vendors-aggregate/stats`, { credentials: "include" }); + const response = await apiFetch(`${config.apiUrl}/vendors-aggregate/stats`, { credentials: "include" }); if (!response.ok) throw new Error("Failed to fetch vendor stats"); return response.json(); }, @@ -628,7 +629,7 @@ function VendorsPanel() { const { data: filterOptions } = useQuery({ queryKey: ["vendorsFilterOptions"], queryFn: async () => { - const response = await fetch(`${config.apiUrl}/vendors-aggregate/filter-options`, { credentials: "include" }); + const response = await apiFetch(`${config.apiUrl}/vendors-aggregate/filter-options`, { credentials: "include" }); if (!response.ok) throw new Error("Failed to fetch filter options"); return response.json(); }, diff --git a/inventory/src/pages/BulkEdit.tsx b/inventory/src/pages/BulkEdit.tsx index 5f4a979..0add765 100644 --- a/inventory/src/pages/BulkEdit.tsx +++ b/inventory/src/pages/BulkEdit.tsx @@ -1,5 +1,7 @@ import { useState, useEffect, useCallback, useMemo, useRef } from "react"; +import { apiFetch } from '@/utils/api'; import axios from "axios"; +import { apiClient } from "@/utils/apiClient"; import { toast } from "sonner"; import { Loader2, Sparkles, Save } from "lucide-react"; import { Button } from "@/components/ui/button"; @@ -169,7 +171,7 @@ export default function BulkEdit() { const hadExisting = allProducts.length > 0; setIsLoadingProducts(true); try { - const res = await axios.get("/api/import/search-products", { + const res = await apiClient.get("/api/import/search-products", { params: { pid: pids.join(",") }, }); const fetched = res.data as SearchProduct[]; @@ -200,7 +202,7 @@ export default function BulkEdit() { setAllProducts([]); setIsLoadingProducts(true); try { - const res = await axios.get(`/api/import/${endpoint}`, { signal: controller.signal }); + const res = await apiClient.get(`/api/import/${endpoint}`, { signal: controller.signal }); setAllProducts(res.data); setPage(1); toast.success(`Loaded ${res.data.length} ${label} products`); @@ -215,7 +217,7 @@ export default function BulkEdit() { if (landingExtras[tabKey]) return; setIsLoadingExtras(true); try { - const res = await axios.get("/api/import/landing-extras", { + const res = await apiClient.get("/api/import/landing-extras", { params: { catId, sid: 0 }, }); setLandingExtras((prev) => ({ ...prev, [tabKey]: res.data })); @@ -234,7 +236,7 @@ export default function BulkEdit() { setAllProducts([]); setIsLoadingProducts(true); try { - const res = await axios.get("/api/import/path-products", { + const res = await apiClient.get("/api/import/path-products", { params: { path: extra.path }, signal: controller.signal, }); @@ -279,7 +281,7 @@ export default function BulkEdit() { try { const params: Record = { company: lineCompany, line: lineLine }; if (lineSubline) params.subline = lineSubline; - const res = await axios.get("/api/import/line-products", { params, signal: controller.signal }); + const res = await apiClient.get("/api/import/line-products", { params, signal: controller.signal }); setAllProducts(res.data); setPage(1); toast.success(`Loaded ${res.data.length} products`); @@ -378,7 +380,7 @@ export default function BulkEdit() { } try { - const response = await fetch(endpoint, { + const response = await apiFetch(endpoint, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ product: payload }), diff --git a/inventory/src/pages/Categories.tsx b/inventory/src/pages/Categories.tsx index 150c5fa..a9739c8 100644 --- a/inventory/src/pages/Categories.tsx +++ b/inventory/src/pages/Categories.tsx @@ -1,4 +1,5 @@ import { useState, useMemo, useCallback, useEffect } from "react"; +import { apiFetch } from '@/utils/api'; import { useQuery } from "@tanstack/react-query"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { @@ -392,7 +393,7 @@ export function Categories() { const url = `${config.apiUrl}/categories-aggregate?${queryParams.toString()}`; console.log("Fetching categories from URL:", url); - const response = await fetch(url, { + const response = await apiFetch(url, { credentials: "include", headers: { Accept: "application/json", @@ -427,7 +428,7 @@ export function Categories() { >({ queryKey: ["categoriesStats"], queryFn: async () => { - const response = await fetch( + const response = await apiFetch( `${config.apiUrl}/categories-aggregate/stats`, { credentials: "include", @@ -444,7 +445,7 @@ export function Categories() { >({ queryKey: ["categoriesFilterOptions"], queryFn: async () => { - const response = await fetch( + const response = await apiFetch( `${config.apiUrl}/categories-aggregate/filter-options`, { credentials: "include", diff --git a/inventory/src/pages/Chat.tsx b/inventory/src/pages/Chat.tsx index d4087c8..00f60ee 100644 --- a/inventory/src/pages/Chat.tsx +++ b/inventory/src/pages/Chat.tsx @@ -1,4 +1,5 @@ import React, { useState, useEffect, useContext } from 'react'; +import { apiFetch } from '@/utils/api'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Input } from '@/components/ui/input'; @@ -63,7 +64,7 @@ export function Chat() { useEffect(() => { const fetchUsers = async () => { try { - const response = await fetch(`${config.chatUrl}/users`); + const response = await apiFetch(`${config.chatUrl}/users`); const data = await response.json(); if (data.status === 'success') { @@ -123,7 +124,7 @@ export function Chat() { setSearching(true); try { - const response = await fetch( + const response = await apiFetch( `${config.chatUrl}/users/${selectedUserId}/search?q=${encodeURIComponent(globalSearchQuery)}&limit=20` ); const data = await response.json(); diff --git a/inventory/src/pages/Forecasting.tsx b/inventory/src/pages/Forecasting.tsx index 30bd526..19bf596 100644 --- a/inventory/src/pages/Forecasting.tsx +++ b/inventory/src/pages/Forecasting.tsx @@ -1,4 +1,5 @@ import { useEffect, useState, useMemo, Fragment } from "react"; +import { apiFetch } from '@/utils/api'; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { useQuery, useQueryClient } from "@tanstack/react-query"; @@ -110,7 +111,7 @@ export default function Forecasting() { const { data: brands = [], isLoading: brandsLoading } = useQuery({ queryKey: ["brands"], queryFn: async () => { - const response = await fetch("/api/products/brands"); + const response = await apiFetch("/api/products/brands"); if (!response.ok) throw new Error("Failed to fetch brands"); const data = await response.json(); return Array.isArray(data) ? data : []; @@ -128,7 +129,7 @@ export default function Forecasting() { startDate: dateRange.from?.toISOString() || "", endDate: dateRange.to?.toISOString() || "", }); - const response = await fetch(`/api/analytics/forecast-v2?${params}`); + const response = await apiFetch(`/api/analytics/forecast-v2?${params}`); if (!response.ok) throw new Error("Failed to fetch forecast data"); return response.json(); }, diff --git a/inventory/src/pages/HtsLookup.tsx b/inventory/src/pages/HtsLookup.tsx index e8faea7..931559a 100644 --- a/inventory/src/pages/HtsLookup.tsx +++ b/inventory/src/pages/HtsLookup.tsx @@ -1,4 +1,5 @@ import { useEffect, useMemo, useRef, useState, type FormEvent, type MouseEvent } from "react"; +import { apiFetch } from '@/utils/api'; import { useQuery } from "@tanstack/react-query"; import { Search, Loader2, PackageOpen, Copy, Check } from "lucide-react"; @@ -63,7 +64,7 @@ export default function HtsLookup() { enabled: false, queryFn: async () => { const params = new URLSearchParams({ search: submittedTerm }); - const response = await fetch(`/api/hts-lookup?${params.toString()}`); + const response = await apiFetch(`/api/hts-lookup?${params.toString()}`); const payload = await response.json().catch(() => ({})); if (!response.ok) { diff --git a/inventory/src/pages/Import.tsx b/inventory/src/pages/Import.tsx index 9305b2c..f9f9397 100644 --- a/inventory/src/pages/Import.tsx +++ b/inventory/src/pages/Import.tsx @@ -1,4 +1,5 @@ import { useState, useContext, useMemo } from "react"; +import { apiFetch } from '@/utils/api'; import { ReactSpreadsheetImport, StepType } from "@/components/product-import"; import type { StepState } from "@/components/product-import/steps/UploadFlow"; import { Button } from "@/components/ui/button"; @@ -276,7 +277,7 @@ export function Import() { const { data: fieldOptions, isLoading: isLoadingOptions } = useQuery({ queryKey: ["import-field-options"], queryFn: async () => { - const response = await fetch(`${config.apiUrl}/import/field-options`); + const response = await apiFetch(`${config.apiUrl}/import/field-options`); if (!response.ok) { throw new Error("Failed to fetch field options"); } @@ -289,7 +290,7 @@ export function Import() { queryKey: ["product-lines", selectedCompany], queryFn: async () => { if (!selectedCompany) return []; - const response = await fetch(`${config.apiUrl}/import/product-lines/${selectedCompany}`); + const response = await apiFetch(`${config.apiUrl}/import/product-lines/${selectedCompany}`); if (!response.ok) { throw new Error("Failed to fetch product lines"); } @@ -303,7 +304,7 @@ export function Import() { queryKey: ["sublines", selectedLine], queryFn: async () => { if (!selectedLine) return []; - const response = await fetch(`${config.apiUrl}/import/sublines/${selectedLine}`); + const response = await apiFetch(`${config.apiUrl}/import/sublines/${selectedLine}`); if (!response.ok) { throw new Error("Failed to fetch sublines"); } diff --git a/inventory/src/pages/ProductEditor.tsx b/inventory/src/pages/ProductEditor.tsx index b6bd040..4487925 100644 --- a/inventory/src/pages/ProductEditor.tsx +++ b/inventory/src/pages/ProductEditor.tsx @@ -1,5 +1,6 @@ import { useState, useEffect, useCallback, useMemo, useRef } from "react"; import axios from "axios"; +import { apiClient } from "@/utils/apiClient"; import { toast } from "sonner"; import { Loader2, RefreshCw } from "lucide-react"; import { Button } from "@/components/ui/button"; @@ -284,7 +285,7 @@ export default function ProductEditor() { const hadExisting = allProducts.length > 0; setIsLoadingProducts(true); try { - const res = await axios.get("/api/import/search-products", { + const res = await apiClient.get("/api/import/search-products", { params: { pid: pids.join(",") }, }); const fetched = res.data as SearchProduct[]; @@ -319,7 +320,7 @@ export default function ProductEditor() { setAllProducts([]); setIsLoadingProducts(true); try { - const res = await axios.get(`/api/import/${endpoint}`, { signal: controller.signal }); + const res = await apiClient.get(`/api/import/${endpoint}`, { signal: controller.signal }); setAllProducts(res.data); setPage(1); toast.success(`Loaded ${res.data.length} ${label} products`); @@ -334,7 +335,7 @@ export default function ProductEditor() { if (landingExtras[tabKey]) return; setIsLoadingExtras(true); try { - const res = await axios.get("/api/import/landing-extras", { + const res = await apiClient.get("/api/import/landing-extras", { params: { catId, sid: 0 }, }); setLandingExtras((prev) => ({ ...prev, [tabKey]: res.data })); @@ -356,7 +357,7 @@ export default function ProductEditor() { setAllProducts([]); setIsLoadingProducts(true); try { - const res = await axios.get("/api/import/path-products", { + const res = await apiClient.get("/api/import/path-products", { params: { path: extra.path }, signal: controller.signal, }); @@ -411,7 +412,7 @@ export default function ProductEditor() { try { const params: Record = { company: lineCompany, line: lineLine }; if (lineSubline) params.subline = lineSubline; - const res = await axios.get("/api/import/line-products", { params, signal: controller.signal }); + const res = await apiClient.get("/api/import/line-products", { params, signal: controller.signal }); setAllProducts(res.data); setPage(1); toast.success(`Loaded ${res.data.length} products`); @@ -432,7 +433,7 @@ export default function ProductEditor() { setQueryStatus(null); setIsLoadingProducts(true); try { - const res = await axios.get("/api/import/query-products", { + const res = await apiClient.get("/api/import/query-products", { params: { query_id: qid }, signal: controller.signal, }); diff --git a/inventory/src/pages/ProductLines.tsx b/inventory/src/pages/ProductLines.tsx index 9fb4db6..70bea82 100644 --- a/inventory/src/pages/ProductLines.tsx +++ b/inventory/src/pages/ProductLines.tsx @@ -1,4 +1,5 @@ import { useState, useMemo, useCallback } from "react"; +import { apiFetch } from '@/utils/api'; import { useQuery } from "@tanstack/react-query"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; @@ -345,7 +346,7 @@ function LineProductDetail({ brand, line }: { brand: string; line: string }) { const { data, isLoading } = useQuery({ queryKey: ['lineProducts', brand, line], queryFn: async () => { - const res = await fetch( + const res = await apiFetch( `${config.apiUrl}/lines-aggregate/${encodeURIComponent(brand)}/${encodeURIComponent(line)}/products`, { credentials: 'include' } ); @@ -484,7 +485,7 @@ export function ProductLines() { const { data: listData, isLoading: isLoadingList, error: listError } = useQuery({ queryKey: ['productLines', queryParams.toString()], queryFn: async () => { - const res = await fetch(`${config.apiUrl}/lines-aggregate?${queryParams.toString()}`, { + const res = await apiFetch(`${config.apiUrl}/lines-aggregate?${queryParams.toString()}`, { credentials: 'include' }); if (!res.ok) throw new Error(`Network response was not ok (${res.status})`); @@ -496,7 +497,7 @@ export function ProductLines() { const { data: statsData, isLoading: isLoadingStats } = useQuery({ queryKey: ['productLineStats'], queryFn: async () => { - const res = await fetch(`${config.apiUrl}/lines-aggregate/stats`, { credentials: 'include' }); + const res = await apiFetch(`${config.apiUrl}/lines-aggregate/stats`, { credentials: 'include' }); if (!res.ok) throw new Error("Failed to fetch stats"); return res.json(); }, @@ -505,7 +506,7 @@ export function ProductLines() { const { data: filterOptions } = useQuery({ queryKey: ['productLineFilterOptions'], queryFn: async () => { - const res = await fetch(`${config.apiUrl}/lines-aggregate/filter-options`, { credentials: 'include' }); + const res = await apiFetch(`${config.apiUrl}/lines-aggregate/filter-options`, { credentials: 'include' }); if (!res.ok) throw new Error("Failed to fetch filter options"); return res.json(); }, diff --git a/inventory/src/pages/Products.tsx b/inventory/src/pages/Products.tsx index 4c98382..e896274 100644 --- a/inventory/src/pages/Products.tsx +++ b/inventory/src/pages/Products.tsx @@ -1,4 +1,5 @@ import React, { useState, useEffect, useMemo, useCallback } from 'react'; +import { apiFetch } from '@/utils/api'; import { useSearchParams } from 'react-router-dom'; import { useQuery, keepPreviousData } from '@tanstack/react-query'; import { motion } from 'framer-motion'; @@ -289,7 +290,7 @@ export function Products() { params.append('showInvisible', 'true'); } - const response = await fetch(`/api/metrics?${params.toString()}`, {credentials: 'include'}); + const response = await apiFetch(`/api/metrics?${params.toString()}`, {credentials: 'include'}); if (!response.ok) throw new Error('Failed to fetch products'); const data = await response.json(); @@ -323,7 +324,7 @@ export function Products() { queryKey: ['filterOptions'], queryFn: async () => { try { - const response = await fetch('/api/metrics/filter-options'); + const response = await apiFetch('/api/metrics/filter-options'); if (!response.ok) throw new Error('Failed to fetch filter options'); const data = await response.json(); @@ -348,7 +349,7 @@ export function Products() { const params = new URLSearchParams(); if (showNonReplenishable) params.append('showNonReplenishable', 'true'); if (showInvisible) params.append('showInvisible', 'true'); - const response = await fetch(`/api/metrics/summary?${params.toString()}`, { credentials: 'include' }); + const response = await apiFetch(`/api/metrics/summary?${params.toString()}`, { credentials: 'include' }); if (!response.ok) throw new Error('Failed to fetch summary'); return response.json(); }, diff --git a/inventory/src/pages/PurchaseOrders.tsx b/inventory/src/pages/PurchaseOrders.tsx index 152b863..90073be 100644 --- a/inventory/src/pages/PurchaseOrders.tsx +++ b/inventory/src/pages/PurchaseOrders.tsx @@ -1,4 +1,5 @@ import { useEffect, useState, useRef, useMemo } from "react"; +import { apiFetch } from '@/utils/api'; import OrderMetricsCard from "../components/purchase-orders/OrderMetricsCard"; import VendorMetricsCard from "../components/purchase-orders/VendorMetricsCard"; import CategoryMetricsCard from "../components/purchase-orders/CategoryMetricsCard"; @@ -151,7 +152,7 @@ export default function PurchaseOrders() { console.log("Fetching data with params:", searchParams.toString()); // Fetch orders first separately to handle errors better - const purchaseOrdersRes = await fetch(`/api/purchase-orders?${searchParams.toString()}`); + const purchaseOrdersRes = await apiFetch(`/api/purchase-orders?${searchParams.toString()}`); if (!purchaseOrdersRes.ok) { const errorText = await purchaseOrdersRes.text(); @@ -179,9 +180,9 @@ export default function PurchaseOrders() { // Now fetch the additional data in parallel const [vendorMetricsRes, costAnalysisRes, deliveryMetricsRes] = await Promise.all([ - fetch("/api/purchase-orders/vendor-metrics"), - fetch("/api/purchase-orders/cost-analysis"), - fetch("/api/purchase-orders/delivery-metrics"), + apiFetch("/api/purchase-orders/vendor-metrics"), + apiFetch("/api/purchase-orders/cost-analysis"), + apiFetch("/api/purchase-orders/delivery-metrics"), ]); let vendorMetricsData = []; @@ -369,8 +370,8 @@ export default function PurchaseOrders() { const dateParam = oneYearAgo.toISOString().split("T")[0]; // Format as YYYY-MM-DD const [vendorResponse, categoryResponse] = await Promise.all([ - fetch(`/api/purchase-orders/vendor-analysis?since=${dateParam}`), - fetch(`/api/purchase-orders/category-analysis?since=${dateParam}`), + apiFetch(`/api/purchase-orders/vendor-analysis?since=${dateParam}`), + apiFetch(`/api/purchase-orders/category-analysis?since=${dateParam}`), ]); if (vendorResponse.ok) { diff --git a/inventory/src/pages/RepeatOrders.tsx b/inventory/src/pages/RepeatOrders.tsx index 281d7b4..a5ea8a2 100644 --- a/inventory/src/pages/RepeatOrders.tsx +++ b/inventory/src/pages/RepeatOrders.tsx @@ -1,4 +1,5 @@ import { Fragment, useMemo, useState } from "react"; +import { apiFetch } from '@/utils/api'; import { useQuery } from "@tanstack/react-query"; import { Loader2, @@ -205,7 +206,7 @@ function ProductHistory({ supplierId: String(supplierId), windowDays: String(windowDays), }); - const res = await fetch( + const res = await apiFetch( `/api/repeat-orders/${pid}/history?${params.toString()}` ); if (!res.ok) throw new Error("Failed to load history"); @@ -370,7 +371,7 @@ export default function RepeatOrders() { const { data: suppliersData } = useQuery<{ suppliers: Supplier[] }>({ queryKey: ["repeat-orders-suppliers"], queryFn: async () => { - const res = await fetch(`/api/repeat-orders/suppliers?windowDays=90`); + const res = await apiFetch(`/api/repeat-orders/suppliers?windowDays=90`); if (!res.ok) throw new Error("Failed to load suppliers"); return res.json(); }, @@ -386,7 +387,7 @@ export default function RepeatOrders() { minPoCount: String(minPoCount), maxAvgQty: String(maxAvgQty), }); - const res = await fetch(`/api/repeat-orders?${params.toString()}`); + const res = await apiFetch(`/api/repeat-orders?${params.toString()}`); if (!res.ok) { const payload = await res.json().catch(() => ({})); throw new Error(payload.error || "Failed to load repeat orders"); diff --git a/inventory/src/pages/SpecLookup.tsx b/inventory/src/pages/SpecLookup.tsx index 9d15eb3..daf654d 100644 --- a/inventory/src/pages/SpecLookup.tsx +++ b/inventory/src/pages/SpecLookup.tsx @@ -1,4 +1,5 @@ import { useEffect, useMemo, useRef, useState, type FormEvent, type MouseEvent } from "react"; +import { apiFetch } from '@/utils/api'; import { useQuery } from "@tanstack/react-query"; import { Search, Loader2, PackageOpen, Copy, Check, ChevronsUpDown, X } from "lucide-react"; @@ -122,7 +123,7 @@ export default function SpecLookup() { const { data: brandsData } = useQuery<{ brands: string[] }>({ queryKey: ["spec-lookup-brands"], queryFn: async () => { - const response = await fetch(`/api/brands-aggregate/filter-options`); + const response = await apiFetch(`/api/brands-aggregate/filter-options`); if (!response.ok) throw new Error("Failed to load brands"); return response.json(); }, @@ -143,7 +144,7 @@ export default function SpecLookup() { if (submitted?.company) params.set("company", submitted.company); if (submitted?.term) params.set("term", submitted.term); - const response = await fetch(`/api/spec-lookup?${params.toString()}`); + const response = await apiFetch(`/api/spec-lookup?${params.toString()}`); const payload = await response.json().catch(() => ({})); if (!response.ok) { diff --git a/inventory/src/services/importAuditLogApi.ts b/inventory/src/services/importAuditLogApi.ts index 2033ac6..a7efcd4 100644 --- a/inventory/src/services/importAuditLogApi.ts +++ b/inventory/src/services/importAuditLogApi.ts @@ -1,3 +1,4 @@ +import { apiFetch } from '@/utils/api'; /** * Import Audit Log API Service * @@ -31,7 +32,7 @@ export interface ImportAuditLogEntry { */ export async function createImportAuditLog(entry: ImportAuditLogEntry): Promise { try { - await fetch(BASE_URL, { + await apiFetch(BASE_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(entry), diff --git a/inventory/src/services/importSessionApi.ts b/inventory/src/services/importSessionApi.ts index 98d2223..7b1aa9a 100644 --- a/inventory/src/services/importSessionApi.ts +++ b/inventory/src/services/importSessionApi.ts @@ -4,6 +4,7 @@ * Handles all API calls for import session persistence. */ +import { apiFetch } from '@/utils/api'; import type { ImportSession, ImportSessionListItem, @@ -36,7 +37,7 @@ async function handleResponse(response: Response): Promise { * List all sessions for a user (named + unnamed) */ export async function listSessions(userId: number): Promise { - const response = await fetch(`${BASE_URL}?user_id=${userId}`); + const response = await apiFetch(`${BASE_URL}?user_id=${userId}`); return handleResponse(response); } @@ -44,7 +45,7 @@ export async function listSessions(userId: number): Promise { - const response = await fetch(`${BASE_URL}/${id}`); + const response = await apiFetch(`${BASE_URL}/${id}`); return handleResponse(response); } @@ -52,7 +53,7 @@ export async function getSession(id: number): Promise { * Create a new named session */ export async function createSession(data: ImportSessionCreateRequest): Promise { - const response = await fetch(BASE_URL, { + const response = await apiFetch(BASE_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), @@ -64,7 +65,7 @@ export async function createSession(data: ImportSessionCreateRequest): Promise { - const response = await fetch(`${BASE_URL}/${id}`, { + const response = await apiFetch(`${BASE_URL}/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), @@ -76,7 +77,7 @@ export async function updateSession(id: number, data: ImportSessionUpdateRequest * Autosave - upsert the unnamed session for a user */ export async function autosaveSession(data: ImportSessionAutosaveRequest): Promise { - const response = await fetch(`${BASE_URL}/autosave`, { + const response = await apiFetch(`${BASE_URL}/autosave`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), @@ -88,7 +89,7 @@ export async function autosaveSession(data: ImportSessionAutosaveRequest): Promi * Delete a session by ID */ export async function deleteSession(id: number): Promise { - const response = await fetch(`${BASE_URL}/${id}`, { + const response = await apiFetch(`${BASE_URL}/${id}`, { method: 'DELETE', }); if (!response.ok) { @@ -108,7 +109,7 @@ export async function deleteSession(id: number): Promise { * Delete the unnamed/autosave session for a user */ export async function deleteAutosaveSession(userId: number): Promise { - const response = await fetch(`${BASE_URL}/autosave/${userId}`, { + const response = await apiFetch(`${BASE_URL}/autosave/${userId}`, { method: 'DELETE', }); // 404 is ok - means no autosave existed diff --git a/inventory/src/services/productEditor.ts b/inventory/src/services/productEditor.ts index 6ee2510..b81aeee 100644 --- a/inventory/src/services/productEditor.ts +++ b/inventory/src/services/productEditor.ts @@ -1,3 +1,4 @@ +import { apiFetch } from '@/utils/api'; export interface ImageChanges { order: (number | string)[]; hidden: number[]; @@ -64,7 +65,7 @@ export async function submitProductEdit({ let response: Response; try { - response = await fetch(targetUrl, fetchOptions); + response = await apiFetch(targetUrl, fetchOptions); } catch (networkError) { throw new Error( networkError instanceof Error ? networkError.message : "Network request failed" @@ -134,7 +135,7 @@ export async function submitTaxonomySet({ let response: Response; try { - response = await fetch(targetUrl, fetchOptions); + response = await apiFetch(targetUrl, fetchOptions); } catch (networkError) { throw new Error( networkError instanceof Error ? networkError.message : "Network request failed" @@ -199,7 +200,7 @@ export async function submitImageChanges({ let response: Response; try { - response = await fetch(targetUrl, fetchOptions); + response = await apiFetch(targetUrl, fetchOptions); } catch (networkError) { throw new Error( networkError instanceof Error ? networkError.message : "Network request failed" diff --git a/inventory/src/services/productEditorAuditLog.ts b/inventory/src/services/productEditorAuditLog.ts index 22ce458..7b12394 100644 --- a/inventory/src/services/productEditorAuditLog.ts +++ b/inventory/src/services/productEditorAuditLog.ts @@ -1,3 +1,4 @@ +import { apiFetch } from '@/utils/api'; /** * Product Editor Audit Log API Service * @@ -27,7 +28,7 @@ export interface ProductEditorAuditLogEntry { */ export async function createProductEditorAuditLog(entry: ProductEditorAuditLogEntry): Promise { try { - await fetch(BASE_URL, { + await apiFetch(BASE_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(entry), diff --git a/inventory/src/utils/api.ts b/inventory/src/utils/api.ts new file mode 100644 index 0000000..a05dc8b --- /dev/null +++ b/inventory/src/utils/api.ts @@ -0,0 +1,35 @@ +// Centralized fetch wrapper that attaches the auth token from localStorage +// and bounces the user to /login on a 401. +// +// Drop-in for `fetch`: same call signature, same return shape. The only +// differences from raw fetch: +// 1. If a token exists in localStorage and the caller hasn't already set an +// Authorization header, we attach `Authorization: Bearer ${token}`. +// 2. If the response is 401 *and* we had a token to begin with, we fire an +// `auth:logout` window event so AuthContext can clear state and redirect +// to /login. (No token + 401 just returns normally — login pages need +// that path.) +// +// We intentionally do NOT set Content-Type. FormData uploads rely on fetch +// auto-generating the multipart boundary, and JSON callers set their own +// header. This wrapper only touches Authorization. + +export async function apiFetch( + input: RequestInfo | URL, + init: RequestInit = {}, +): Promise { + const token = localStorage.getItem('token'); + const headers = new Headers(init.headers); + + if (token && !headers.has('Authorization')) { + headers.set('Authorization', `Bearer ${token}`); + } + + const res = await fetch(input, { ...init, headers }); + + if (res.status === 401 && token) { + window.dispatchEvent(new Event('auth:logout')); + } + + return res; +} diff --git a/inventory/src/utils/apiClient.ts b/inventory/src/utils/apiClient.ts new file mode 100644 index 0000000..2418dd8 --- /dev/null +++ b/inventory/src/utils/apiClient.ts @@ -0,0 +1,30 @@ +import axios from 'axios'; + +// Centralized axios instance that mirrors apiFetch's behavior: it injects the +// auth token from localStorage into every outbound request and dispatches an +// `auth:logout` window event on 401 responses (so AuthContext clears state). +// +// Drop-in for the default `axios` export: use `apiClient.get(...)` etc. in +// place of `axios.get(...)`. The interceptors handle the auth plumbing. + +export const apiClient = axios.create(); + +apiClient.interceptors.request.use((config) => { + const token = localStorage.getItem('token'); + if (token && !config.headers.has('Authorization')) { + config.headers.set('Authorization', `Bearer ${token}`); + } + return config; +}); + +apiClient.interceptors.response.use( + (response) => response, + (error) => { + if (error?.response?.status === 401 && localStorage.getItem('token')) { + window.dispatchEvent(new Event('auth:logout')); + } + return Promise.reject(error); + }, +); + +export default apiClient; diff --git a/inventory/tsconfig.tsbuildinfo b/inventory/tsconfig.tsbuildinfo index 8fe1170..081809d 100644 --- a/inventory/tsconfig.tsbuildinfo +++ b/inventory/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/app.tsx","./src/config.ts","./src/main.tsx","./src/vite-env.d.ts","./src/components/config.ts","./src/components/ai/aidescriptioncompare.tsx","./src/components/analytics/agingsellthrough.tsx","./src/components/analytics/capitalefficiency.tsx","./src/components/analytics/discountimpact.tsx","./src/components/analytics/growthmomentum.tsx","./src/components/analytics/inventoryflow.tsx","./src/components/analytics/inventorytrends.tsx","./src/components/analytics/inventoryvaluetrend.tsx","./src/components/analytics/portfolioanalysis.tsx","./src/components/analytics/seasonalpatterns.tsx","./src/components/analytics/stockhealth.tsx","./src/components/analytics/stockoutrisk.tsx","./src/components/auth/firstaccessiblepage.tsx","./src/components/auth/protected.tsx","./src/components/auth/requireauth.tsx","./src/components/bulk-edit/bulkeditrow.tsx","./src/components/chat/chatroom.tsx","./src/components/chat/chattest.tsx","./src/components/chat/roomlist.tsx","./src/components/chat/searchresults.tsx","./src/components/create-po/addproductsdialog.tsx","./src/components/create-po/confirmationview.tsx","./src/components/create-po/lineitemstable.tsx","./src/components/create-po/pofloatingselectionbar.tsx","./src/components/create-po/reviewmatchesdialog.tsx","./src/components/create-po/supplierselector.tsx","./src/components/create-po/constants.ts","./src/components/create-po/parsespreadsheet.ts","./src/components/create-po/resolveidentifiers.ts","./src/components/create-po/types.ts","./src/components/dashboard/financialoverview.tsx","./src/components/dashboard/operationsmetrics.tsx","./src/components/dashboard/payrollmetrics.tsx","./src/components/dashboard/periodselectionpopover.tsx","./src/components/dashboard/shared/dashboardbadge.tsx","./src/components/dashboard/shared/dashboardcharttooltip.tsx","./src/components/dashboard/shared/dashboardmultistatcardmini.tsx","./src/components/dashboard/shared/dashboardsectionheader.tsx","./src/components/dashboard/shared/dashboardskeleton.tsx","./src/components/dashboard/shared/dashboardstatcard.tsx","./src/components/dashboard/shared/dashboardstatcardmini.tsx","./src/components/dashboard/shared/dashboardstates.tsx","./src/components/dashboard/shared/dashboardtable.tsx","./src/components/dashboard/shared/index.ts","./src/components/discount-simulator/configpanel.tsx","./src/components/discount-simulator/resultschart.tsx","./src/components/discount-simulator/resultstable.tsx","./src/components/discount-simulator/summarycard.tsx","./src/components/forecasting/daterangepickerquick.tsx","./src/components/forecasting/columns.tsx","./src/components/layout/appsidebar.tsx","./src/components/layout/mainlayout.tsx","./src/components/layout/navuser.tsx","./src/components/newsletter/campaignhistorydialog.tsx","./src/components/newsletter/newsletterstats.tsx","./src/components/newsletter/recommendationtable.tsx","./src/components/overview/bestsellers.tsx","./src/components/overview/forecastaccuracy.tsx","./src/components/overview/forecastmetrics.tsx","./src/components/overview/overstockmetrics.tsx","./src/components/overview/purchasemetrics.tsx","./src/components/overview/replenishmentmetrics.tsx","./src/components/overview/salesmetrics.tsx","./src/components/overview/stockmetrics.tsx","./src/components/overview/topoverstockedproducts.tsx","./src/components/overview/topreplenishproducts.tsx","./src/components/product-editor/comboboxfield.tsx","./src/components/product-editor/editablecomboboxfield.tsx","./src/components/product-editor/editableinput.tsx","./src/components/product-editor/editablemultiselect.tsx","./src/components/product-editor/imagemanager.tsx","./src/components/product-editor/producteditform.tsx","./src/components/product-editor/productsearch.tsx","./src/components/product-editor/types.ts","./src/components/product-editor/useproductsuggestions.ts","./src/components/product-import/createproductcategorydialog.tsx","./src/components/product-import/reactspreadsheetimport.tsx","./src/components/product-import/config.ts","./src/components/product-import/index.ts","./src/components/product-import/translationsrsiprops.ts","./src/components/product-import/types.ts","./src/components/product-import/components/closeconfirmationdialog.tsx","./src/components/product-import/components/modalwrapper.tsx","./src/components/product-import/components/providers.tsx","./src/components/product-import/components/savesessiondialog.tsx","./src/components/product-import/components/savedsessionslist.tsx","./src/components/product-import/components/table.tsx","./src/components/product-import/hooks/usersi.ts","./src/components/product-import/steps/steps.tsx","./src/components/product-import/steps/uploadflow.tsx","./src/components/product-import/steps/imageuploadstep/imageuploadstep.tsx","./src/components/product-import/steps/imageuploadstep/types.ts","./src/components/product-import/steps/imageuploadstep/components/droppablecontainer.tsx","./src/components/product-import/steps/imageuploadstep/components/genericdropzone.tsx","./src/components/product-import/steps/imageuploadstep/components/unassignedimagessection.tsx","./src/components/product-import/steps/imageuploadstep/components/productcard/copybutton.tsx","./src/components/product-import/steps/imageuploadstep/components/productcard/imagedropzone.tsx","./src/components/product-import/steps/imageuploadstep/components/productcard/productcard.tsx","./src/components/product-import/steps/imageuploadstep/components/productcard/sortableimage.tsx","./src/components/product-import/steps/imageuploadstep/components/unassignedimagessection/unassignedimageitem.tsx","./src/components/product-import/steps/imageuploadstep/hooks/usebulkimageupload.ts","./src/components/product-import/steps/imageuploadstep/hooks/usedraganddrop.ts","./src/components/product-import/steps/imageuploadstep/hooks/useproductimageoperations.ts","./src/components/product-import/steps/imageuploadstep/hooks/useproductimagesinit.ts","./src/components/product-import/steps/imageuploadstep/hooks/useurlimageupload.ts","./src/components/product-import/steps/matchcolumnsstep/matchcolumnsstep.tsx","./src/components/product-import/steps/matchcolumnsstep/types.ts","./src/components/product-import/steps/matchcolumnsstep/components/matchicon.tsx","./src/components/product-import/steps/matchcolumnsstep/components/templatecolumn.tsx","./src/components/product-import/steps/matchcolumnsstep/utils/findmatch.ts","./src/components/product-import/steps/matchcolumnsstep/utils/findunmatchedrequiredfields.ts","./src/components/product-import/steps/matchcolumnsstep/utils/getfieldoptions.ts","./src/components/product-import/steps/matchcolumnsstep/utils/getmatchedcolumns.ts","./src/components/product-import/steps/matchcolumnsstep/utils/normalizecheckboxvalue.ts","./src/components/product-import/steps/matchcolumnsstep/utils/normalizetabledata.ts","./src/components/product-import/steps/matchcolumnsstep/utils/setcolumn.ts","./src/components/product-import/steps/matchcolumnsstep/utils/setignorecolumn.ts","./src/components/product-import/steps/matchcolumnsstep/utils/setsubcolumn.ts","./src/components/product-import/steps/matchcolumnsstep/utils/uniqueentries.ts","./src/components/product-import/steps/selectheaderstep/selectheaderstep.tsx","./src/components/product-import/steps/selectheaderstep/components/selectheadertable.tsx","./src/components/product-import/steps/selectheaderstep/components/columns.tsx","./src/components/product-import/steps/selectsheetstep/selectsheetstep.tsx","./src/components/product-import/steps/uploadstep/uploadstep.tsx","./src/components/product-import/steps/uploadstep/components/dropzone.tsx","./src/components/product-import/steps/uploadstep/components/columns.tsx","./src/components/product-import/steps/uploadstep/utils/readfilesasync.ts","./src/components/product-import/steps/validationstep/index.tsx","./src/components/product-import/steps/validationstep/components/aisuggestionbadge.tsx","./src/components/product-import/steps/validationstep/components/copydownbanner.tsx","./src/components/product-import/steps/validationstep/components/floatingselectionbar.tsx","./src/components/product-import/steps/validationstep/components/initializingoverlay.tsx","./src/components/product-import/steps/validationstep/components/searchabletemplateselect.tsx","./src/components/product-import/steps/validationstep/components/suggestionbadges.tsx","./src/components/product-import/steps/validationstep/components/validationcontainer.tsx","./src/components/product-import/steps/validationstep/components/validationfooter.tsx","./src/components/product-import/steps/validationstep/components/validationtable.tsx","./src/components/product-import/steps/validationstep/components/validationtoolbar.tsx","./src/components/product-import/steps/validationstep/components/cells/checkboxcell.tsx","./src/components/product-import/steps/validationstep/components/cells/comboboxcell.tsx","./src/components/product-import/steps/validationstep/components/cells/inputcell.tsx","./src/components/product-import/steps/validationstep/components/cells/multiselectcell.tsx","./src/components/product-import/steps/validationstep/components/cells/multilineinput.tsx","./src/components/product-import/steps/validationstep/components/cells/selectcell.tsx","./src/components/product-import/steps/validationstep/contexts/aisuggestionscontext.tsx","./src/components/product-import/steps/validationstep/dialogs/aidebugdialog.tsx","./src/components/product-import/steps/validationstep/dialogs/aivalidationprogress.tsx","./src/components/product-import/steps/validationstep/dialogs/aivalidationresults.tsx","./src/components/product-import/steps/validationstep/dialogs/sanitycheckdialog.tsx","./src/components/product-import/steps/validationstep/hooks/useautoinlineaivalidation.ts","./src/components/product-import/steps/validationstep/hooks/usecopydownvalidation.ts","./src/components/product-import/steps/validationstep/hooks/usefieldoptions.ts","./src/components/product-import/steps/validationstep/hooks/useinlineaivalidation.ts","./src/components/product-import/steps/validationstep/hooks/useproductlines.ts","./src/components/product-import/steps/validationstep/hooks/usesanitycheck.ts","./src/components/product-import/steps/validationstep/hooks/usetemplatemanagement.ts","./src/components/product-import/steps/validationstep/hooks/useupcvalidation.ts","./src/components/product-import/steps/validationstep/hooks/usevalidationactions.ts","./src/components/product-import/steps/validationstep/hooks/useaivalidation/index.ts","./src/components/product-import/steps/validationstep/hooks/useaivalidation/useaiapi.ts","./src/components/product-import/steps/validationstep/hooks/useaivalidation/useaiprogress.ts","./src/components/product-import/steps/validationstep/hooks/useaivalidation/useaitransform.ts","./src/components/product-import/steps/validationstep/store/selectors.ts","./src/components/product-import/steps/validationstep/store/types.ts","./src/components/product-import/steps/validationstep/store/validationstore.ts","./src/components/product-import/steps/validationstep/utils/aivalidationutils.ts","./src/components/product-import/steps/validationstep/utils/countryutils.ts","./src/components/product-import/steps/validationstep/utils/datamutations.ts","./src/components/product-import/steps/validationstep/utils/inlineaipayload.ts","./src/components/product-import/steps/validationstep/utils/priceutils.ts","./src/components/product-import/steps/validationstep/utils/upcutils.ts","./src/components/product-import/utils/exceedsmaxrecords.ts","./src/components/product-import/utils/mapdata.ts","./src/components/product-import/utils/mapworkbook.ts","./src/components/product-import/utils/steps.ts","./src/components/products/productdetail.tsx","./src/components/products/productfilters.tsx","./src/components/products/productsummarycards.tsx","./src/components/products/producttable.tsx","./src/components/products/producttableskeleton.tsx","./src/components/products/productviews.tsx","./src/components/products/statusbadge.tsx","./src/components/products/columndefinitions.ts","./src/components/purchase-orders/categorymetricscard.tsx","./src/components/purchase-orders/filtercontrols.tsx","./src/components/purchase-orders/ordermetricscard.tsx","./src/components/purchase-orders/paginationcontrols.tsx","./src/components/purchase-orders/pipelinecard.tsx","./src/components/purchase-orders/purchaseorderaccordion.tsx","./src/components/purchase-orders/purchaseorderstable.tsx","./src/components/purchase-orders/vendormetricscard.tsx","./src/components/settings/auditlog.tsx","./src/components/settings/datamanagement.tsx","./src/components/settings/globalsettings.tsx","./src/components/settings/permissionselector.tsx","./src/components/settings/productsettings.tsx","./src/components/settings/promptmanagement.tsx","./src/components/settings/reusableimagemanagement.tsx","./src/components/settings/templatemanagement.tsx","./src/components/settings/userform.tsx","./src/components/settings/userlist.tsx","./src/components/settings/usermanagement.tsx","./src/components/settings/vendorsettings.tsx","./src/components/templates/searchproducttemplatedialog.tsx","./src/components/templates/templateform.tsx","./src/components/ui/accordion.tsx","./src/components/ui/alert-dialog.tsx","./src/components/ui/alert.tsx","./src/components/ui/avatar.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/calendar.tsx","./src/components/ui/card.tsx","./src/components/ui/carousel.tsx","./src/components/ui/checkbox.tsx","./src/components/ui/code.tsx","./src/components/ui/collapsible.tsx","./src/components/ui/command.tsx","./src/components/ui/date-range-picker-narrow.tsx","./src/components/ui/date-range-picker.tsx","./src/components/ui/dialog.tsx","./src/components/ui/drawer.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/form.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/page-loading.tsx","./src/components/ui/pagination.tsx","./src/components/ui/popover.tsx","./src/components/ui/progress.tsx","./src/components/ui/radio-group.tsx","./src/components/ui/scroll-area.tsx","./src/components/ui/select.tsx","./src/components/ui/separator.tsx","./src/components/ui/sheet.tsx","./src/components/ui/sidebar.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/sonner.tsx","./src/components/ui/switch.tsx","./src/components/ui/table.tsx","./src/components/ui/tabs.tsx","./src/components/ui/textarea.tsx","./src/components/ui/toast.tsx","./src/components/ui/toaster.tsx","./src/components/ui/toggle-group.tsx","./src/components/ui/toggle.tsx","./src/components/ui/tooltip.tsx","./src/config/dashboard.ts","./src/config/uploads.ts","./src/contexts/authcontext.tsx","./src/contexts/dashboardscrollcontext.tsx","./src/contexts/importsessioncontext.tsx","./src/hooks/use-mobile.tsx","./src/hooks/use-toast.ts","./src/hooks/usedebounce.ts","./src/hooks/useimportautosave.ts","./src/lib/utils.ts","./src/lib/dashboard/chartconfig.ts","./src/lib/dashboard/designtokens.ts","./src/pages/analytics.tsx","./src/pages/blackfridaydashboard.tsx","./src/pages/brands.tsx","./src/pages/bulkedit.tsx","./src/pages/categories.tsx","./src/pages/chat.tsx","./src/pages/createpurchaseorder.tsx","./src/pages/dashboard.tsx","./src/pages/discountsimulator.tsx","./src/pages/forecasting.tsx","./src/pages/htslookup.tsx","./src/pages/import.tsx","./src/pages/login.tsx","./src/pages/newsletter.tsx","./src/pages/overview.tsx","./src/pages/producteditor.tsx","./src/pages/productlines.tsx","./src/pages/products.tsx","./src/pages/purchaseorders.tsx","./src/pages/repeatorders.tsx","./src/pages/settings.tsx","./src/pages/smalldashboard.tsx","./src/pages/speclookup.tsx","./src/services/apiv2.ts","./src/services/importauditlogapi.ts","./src/services/importsessionapi.ts","./src/services/producteditor.ts","./src/services/producteditorauditlog.ts","./src/types/dashboard-shims.d.ts","./src/types/dashboard.d.ts","./src/types/discount-simulator.ts","./src/types/globals.d.ts","./src/types/importsession.ts","./src/types/products.ts","./src/types/react-data-grid.d.ts","./src/types/status-codes.ts","./src/utils/emojiutils.ts","./src/utils/formatcurrency.ts","./src/utils/lifecyclephases.ts","./src/utils/naturallanguageperiod.ts","./src/utils/productutils.ts","./src/utils/transformutils.ts"],"version":"5.6.3"} \ No newline at end of file +{"root":["./src/app.tsx","./src/config.ts","./src/main.tsx","./src/vite-env.d.ts","./src/components/config.ts","./src/components/ai/aidescriptioncompare.tsx","./src/components/analytics/agingsellthrough.tsx","./src/components/analytics/capitalefficiency.tsx","./src/components/analytics/discountimpact.tsx","./src/components/analytics/growthmomentum.tsx","./src/components/analytics/inventoryflow.tsx","./src/components/analytics/inventorytrends.tsx","./src/components/analytics/inventoryvaluetrend.tsx","./src/components/analytics/portfolioanalysis.tsx","./src/components/analytics/seasonalpatterns.tsx","./src/components/analytics/stockhealth.tsx","./src/components/analytics/stockoutrisk.tsx","./src/components/auth/firstaccessiblepage.tsx","./src/components/auth/protected.tsx","./src/components/auth/requireauth.tsx","./src/components/bulk-edit/bulkeditrow.tsx","./src/components/chat/chatroom.tsx","./src/components/chat/chattest.tsx","./src/components/chat/roomlist.tsx","./src/components/chat/searchresults.tsx","./src/components/create-po/addproductsdialog.tsx","./src/components/create-po/confirmationview.tsx","./src/components/create-po/lineitemstable.tsx","./src/components/create-po/pofloatingselectionbar.tsx","./src/components/create-po/reviewmatchesdialog.tsx","./src/components/create-po/supplierselector.tsx","./src/components/create-po/constants.ts","./src/components/create-po/parsespreadsheet.ts","./src/components/create-po/resolveidentifiers.ts","./src/components/create-po/types.ts","./src/components/dashboard/financialoverview.tsx","./src/components/dashboard/operationsmetrics.tsx","./src/components/dashboard/payrollmetrics.tsx","./src/components/dashboard/periodselectionpopover.tsx","./src/components/dashboard/shared/dashboardbadge.tsx","./src/components/dashboard/shared/dashboardcharttooltip.tsx","./src/components/dashboard/shared/dashboardmultistatcardmini.tsx","./src/components/dashboard/shared/dashboardsectionheader.tsx","./src/components/dashboard/shared/dashboardskeleton.tsx","./src/components/dashboard/shared/dashboardstatcard.tsx","./src/components/dashboard/shared/dashboardstatcardmini.tsx","./src/components/dashboard/shared/dashboardstates.tsx","./src/components/dashboard/shared/dashboardtable.tsx","./src/components/dashboard/shared/index.ts","./src/components/discount-simulator/configpanel.tsx","./src/components/discount-simulator/resultschart.tsx","./src/components/discount-simulator/resultstable.tsx","./src/components/discount-simulator/summarycard.tsx","./src/components/forecasting/daterangepickerquick.tsx","./src/components/forecasting/columns.tsx","./src/components/layout/appsidebar.tsx","./src/components/layout/mainlayout.tsx","./src/components/layout/navuser.tsx","./src/components/newsletter/campaignhistorydialog.tsx","./src/components/newsletter/newsletterstats.tsx","./src/components/newsletter/recommendationtable.tsx","./src/components/overview/bestsellers.tsx","./src/components/overview/forecastaccuracy.tsx","./src/components/overview/forecastmetrics.tsx","./src/components/overview/overstockmetrics.tsx","./src/components/overview/purchasemetrics.tsx","./src/components/overview/replenishmentmetrics.tsx","./src/components/overview/salesmetrics.tsx","./src/components/overview/stockmetrics.tsx","./src/components/overview/topoverstockedproducts.tsx","./src/components/overview/topreplenishproducts.tsx","./src/components/product-editor/comboboxfield.tsx","./src/components/product-editor/editablecomboboxfield.tsx","./src/components/product-editor/editableinput.tsx","./src/components/product-editor/editablemultiselect.tsx","./src/components/product-editor/imagemanager.tsx","./src/components/product-editor/producteditform.tsx","./src/components/product-editor/productsearch.tsx","./src/components/product-editor/types.ts","./src/components/product-editor/useproductsuggestions.ts","./src/components/product-import/createproductcategorydialog.tsx","./src/components/product-import/reactspreadsheetimport.tsx","./src/components/product-import/config.ts","./src/components/product-import/index.ts","./src/components/product-import/translationsrsiprops.ts","./src/components/product-import/types.ts","./src/components/product-import/components/closeconfirmationdialog.tsx","./src/components/product-import/components/modalwrapper.tsx","./src/components/product-import/components/providers.tsx","./src/components/product-import/components/savesessiondialog.tsx","./src/components/product-import/components/savedsessionslist.tsx","./src/components/product-import/components/table.tsx","./src/components/product-import/hooks/usersi.ts","./src/components/product-import/steps/steps.tsx","./src/components/product-import/steps/uploadflow.tsx","./src/components/product-import/steps/imageuploadstep/imageuploadstep.tsx","./src/components/product-import/steps/imageuploadstep/types.ts","./src/components/product-import/steps/imageuploadstep/components/droppablecontainer.tsx","./src/components/product-import/steps/imageuploadstep/components/genericdropzone.tsx","./src/components/product-import/steps/imageuploadstep/components/unassignedimagessection.tsx","./src/components/product-import/steps/imageuploadstep/components/productcard/copybutton.tsx","./src/components/product-import/steps/imageuploadstep/components/productcard/imagedropzone.tsx","./src/components/product-import/steps/imageuploadstep/components/productcard/productcard.tsx","./src/components/product-import/steps/imageuploadstep/components/productcard/sortableimage.tsx","./src/components/product-import/steps/imageuploadstep/components/unassignedimagessection/unassignedimageitem.tsx","./src/components/product-import/steps/imageuploadstep/hooks/usebulkimageupload.ts","./src/components/product-import/steps/imageuploadstep/hooks/usedraganddrop.ts","./src/components/product-import/steps/imageuploadstep/hooks/useproductimageoperations.ts","./src/components/product-import/steps/imageuploadstep/hooks/useproductimagesinit.ts","./src/components/product-import/steps/imageuploadstep/hooks/useurlimageupload.ts","./src/components/product-import/steps/matchcolumnsstep/matchcolumnsstep.tsx","./src/components/product-import/steps/matchcolumnsstep/types.ts","./src/components/product-import/steps/matchcolumnsstep/components/matchicon.tsx","./src/components/product-import/steps/matchcolumnsstep/components/templatecolumn.tsx","./src/components/product-import/steps/matchcolumnsstep/utils/findmatch.ts","./src/components/product-import/steps/matchcolumnsstep/utils/findunmatchedrequiredfields.ts","./src/components/product-import/steps/matchcolumnsstep/utils/getfieldoptions.ts","./src/components/product-import/steps/matchcolumnsstep/utils/getmatchedcolumns.ts","./src/components/product-import/steps/matchcolumnsstep/utils/normalizecheckboxvalue.ts","./src/components/product-import/steps/matchcolumnsstep/utils/normalizetabledata.ts","./src/components/product-import/steps/matchcolumnsstep/utils/setcolumn.ts","./src/components/product-import/steps/matchcolumnsstep/utils/setignorecolumn.ts","./src/components/product-import/steps/matchcolumnsstep/utils/setsubcolumn.ts","./src/components/product-import/steps/matchcolumnsstep/utils/uniqueentries.ts","./src/components/product-import/steps/selectheaderstep/selectheaderstep.tsx","./src/components/product-import/steps/selectheaderstep/components/selectheadertable.tsx","./src/components/product-import/steps/selectheaderstep/components/columns.tsx","./src/components/product-import/steps/selectsheetstep/selectsheetstep.tsx","./src/components/product-import/steps/uploadstep/uploadstep.tsx","./src/components/product-import/steps/uploadstep/components/dropzone.tsx","./src/components/product-import/steps/uploadstep/components/columns.tsx","./src/components/product-import/steps/uploadstep/utils/readfilesasync.ts","./src/components/product-import/steps/validationstep/index.tsx","./src/components/product-import/steps/validationstep/components/aisuggestionbadge.tsx","./src/components/product-import/steps/validationstep/components/copydownbanner.tsx","./src/components/product-import/steps/validationstep/components/floatingselectionbar.tsx","./src/components/product-import/steps/validationstep/components/initializingoverlay.tsx","./src/components/product-import/steps/validationstep/components/searchabletemplateselect.tsx","./src/components/product-import/steps/validationstep/components/suggestionbadges.tsx","./src/components/product-import/steps/validationstep/components/validationcontainer.tsx","./src/components/product-import/steps/validationstep/components/validationfooter.tsx","./src/components/product-import/steps/validationstep/components/validationtable.tsx","./src/components/product-import/steps/validationstep/components/validationtoolbar.tsx","./src/components/product-import/steps/validationstep/components/cells/checkboxcell.tsx","./src/components/product-import/steps/validationstep/components/cells/comboboxcell.tsx","./src/components/product-import/steps/validationstep/components/cells/inputcell.tsx","./src/components/product-import/steps/validationstep/components/cells/multiselectcell.tsx","./src/components/product-import/steps/validationstep/components/cells/multilineinput.tsx","./src/components/product-import/steps/validationstep/components/cells/selectcell.tsx","./src/components/product-import/steps/validationstep/contexts/aisuggestionscontext.tsx","./src/components/product-import/steps/validationstep/dialogs/aidebugdialog.tsx","./src/components/product-import/steps/validationstep/dialogs/aivalidationprogress.tsx","./src/components/product-import/steps/validationstep/dialogs/aivalidationresults.tsx","./src/components/product-import/steps/validationstep/dialogs/sanitycheckdialog.tsx","./src/components/product-import/steps/validationstep/hooks/useautoinlineaivalidation.ts","./src/components/product-import/steps/validationstep/hooks/usecopydownvalidation.ts","./src/components/product-import/steps/validationstep/hooks/usefieldoptions.ts","./src/components/product-import/steps/validationstep/hooks/useinlineaivalidation.ts","./src/components/product-import/steps/validationstep/hooks/useproductlines.ts","./src/components/product-import/steps/validationstep/hooks/usesanitycheck.ts","./src/components/product-import/steps/validationstep/hooks/usetemplatemanagement.ts","./src/components/product-import/steps/validationstep/hooks/useupcvalidation.ts","./src/components/product-import/steps/validationstep/hooks/usevalidationactions.ts","./src/components/product-import/steps/validationstep/hooks/useaivalidation/index.ts","./src/components/product-import/steps/validationstep/hooks/useaivalidation/useaiapi.ts","./src/components/product-import/steps/validationstep/hooks/useaivalidation/useaiprogress.ts","./src/components/product-import/steps/validationstep/hooks/useaivalidation/useaitransform.ts","./src/components/product-import/steps/validationstep/store/selectors.ts","./src/components/product-import/steps/validationstep/store/types.ts","./src/components/product-import/steps/validationstep/store/validationstore.ts","./src/components/product-import/steps/validationstep/utils/aivalidationutils.ts","./src/components/product-import/steps/validationstep/utils/countryutils.ts","./src/components/product-import/steps/validationstep/utils/datamutations.ts","./src/components/product-import/steps/validationstep/utils/inlineaipayload.ts","./src/components/product-import/steps/validationstep/utils/priceutils.ts","./src/components/product-import/steps/validationstep/utils/upcutils.ts","./src/components/product-import/utils/exceedsmaxrecords.ts","./src/components/product-import/utils/mapdata.ts","./src/components/product-import/utils/mapworkbook.ts","./src/components/product-import/utils/steps.ts","./src/components/products/productdetail.tsx","./src/components/products/productfilters.tsx","./src/components/products/productsummarycards.tsx","./src/components/products/producttable.tsx","./src/components/products/producttableskeleton.tsx","./src/components/products/productviews.tsx","./src/components/products/statusbadge.tsx","./src/components/products/columndefinitions.ts","./src/components/purchase-orders/categorymetricscard.tsx","./src/components/purchase-orders/filtercontrols.tsx","./src/components/purchase-orders/ordermetricscard.tsx","./src/components/purchase-orders/paginationcontrols.tsx","./src/components/purchase-orders/pipelinecard.tsx","./src/components/purchase-orders/purchaseorderaccordion.tsx","./src/components/purchase-orders/purchaseorderstable.tsx","./src/components/purchase-orders/vendormetricscard.tsx","./src/components/settings/auditlog.tsx","./src/components/settings/datamanagement.tsx","./src/components/settings/globalsettings.tsx","./src/components/settings/permissionselector.tsx","./src/components/settings/productsettings.tsx","./src/components/settings/promptmanagement.tsx","./src/components/settings/reusableimagemanagement.tsx","./src/components/settings/templatemanagement.tsx","./src/components/settings/userform.tsx","./src/components/settings/userlist.tsx","./src/components/settings/usermanagement.tsx","./src/components/settings/vendorsettings.tsx","./src/components/templates/searchproducttemplatedialog.tsx","./src/components/templates/templateform.tsx","./src/components/ui/accordion.tsx","./src/components/ui/alert-dialog.tsx","./src/components/ui/alert.tsx","./src/components/ui/avatar.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/calendar.tsx","./src/components/ui/card.tsx","./src/components/ui/carousel.tsx","./src/components/ui/checkbox.tsx","./src/components/ui/code.tsx","./src/components/ui/collapsible.tsx","./src/components/ui/command.tsx","./src/components/ui/date-range-picker-narrow.tsx","./src/components/ui/date-range-picker.tsx","./src/components/ui/dialog.tsx","./src/components/ui/drawer.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/form.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/page-loading.tsx","./src/components/ui/pagination.tsx","./src/components/ui/popover.tsx","./src/components/ui/progress.tsx","./src/components/ui/radio-group.tsx","./src/components/ui/scroll-area.tsx","./src/components/ui/select.tsx","./src/components/ui/separator.tsx","./src/components/ui/sheet.tsx","./src/components/ui/sidebar.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/sonner.tsx","./src/components/ui/switch.tsx","./src/components/ui/table.tsx","./src/components/ui/tabs.tsx","./src/components/ui/textarea.tsx","./src/components/ui/toast.tsx","./src/components/ui/toaster.tsx","./src/components/ui/toggle-group.tsx","./src/components/ui/toggle.tsx","./src/components/ui/tooltip.tsx","./src/config/dashboard.ts","./src/config/uploads.ts","./src/contexts/authcontext.tsx","./src/contexts/dashboardscrollcontext.tsx","./src/contexts/importsessioncontext.tsx","./src/hooks/use-mobile.tsx","./src/hooks/use-toast.ts","./src/hooks/usedebounce.ts","./src/hooks/useimportautosave.ts","./src/lib/utils.ts","./src/lib/dashboard/chartconfig.ts","./src/lib/dashboard/designtokens.ts","./src/pages/analytics.tsx","./src/pages/blackfridaydashboard.tsx","./src/pages/brands.tsx","./src/pages/bulkedit.tsx","./src/pages/categories.tsx","./src/pages/chat.tsx","./src/pages/createpurchaseorder.tsx","./src/pages/dashboard.tsx","./src/pages/discountsimulator.tsx","./src/pages/forecasting.tsx","./src/pages/htslookup.tsx","./src/pages/import.tsx","./src/pages/login.tsx","./src/pages/newsletter.tsx","./src/pages/overview.tsx","./src/pages/producteditor.tsx","./src/pages/productlines.tsx","./src/pages/products.tsx","./src/pages/purchaseorders.tsx","./src/pages/repeatorders.tsx","./src/pages/settings.tsx","./src/pages/smalldashboard.tsx","./src/pages/speclookup.tsx","./src/services/apiv2.ts","./src/services/importauditlogapi.ts","./src/services/importsessionapi.ts","./src/services/producteditor.ts","./src/services/producteditorauditlog.ts","./src/types/dashboard-shims.d.ts","./src/types/dashboard.d.ts","./src/types/discount-simulator.ts","./src/types/globals.d.ts","./src/types/importsession.ts","./src/types/products.ts","./src/types/react-data-grid.d.ts","./src/types/status-codes.ts","./src/utils/api.ts","./src/utils/apiclient.ts","./src/utils/emojiutils.ts","./src/utils/formatcurrency.ts","./src/utils/lifecyclephases.ts","./src/utils/naturallanguageperiod.ts","./src/utils/productutils.ts","./src/utils/transformutils.ts"],"version":"5.6.3"} \ No newline at end of file