Frontend changes (phase F1)

This commit is contained in:
2026-05-23 23:18:16 -04:00
parent 82e568d455
commit 4be0f877fa
90 changed files with 367 additions and 223 deletions
+2 -2
View File
@@ -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
@@ -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<AgingCohort[]>({
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();
},
@@ -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<EfficiencyData>({
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();
},
@@ -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<DiscountRow[]>({
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();
},
@@ -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<GrowthData>({
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();
},
@@ -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<FlowPoint[]>({
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();
},
@@ -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<TrendPoint[]>({
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();
},
@@ -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<ValuePoint[]>({
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();
},
@@ -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<PortfolioData>({
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();
},
@@ -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<SeasonalData>({
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();
},
@@ -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<StockHealthData>({
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();
},
@@ -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<StockoutRiskData>({
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();
},
+5 -4
View File
@@ -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();
+2 -1
View File
@@ -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') {
+2 -1
View File
@@ -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') {
@@ -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<SearchProduct[]>("/api/import/search-products", {
const res = await apiClient.get<SearchProduct[]>("/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<SearchProduct[]>("/api/import/search-products", {
const res = await apiClient.get<SearchProduct[]>("/api/import/search-products", {
params: { pid: pids.join(",") },
});
const items = (res.data ?? []).map((p) => ({
@@ -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<FieldOptionsResponse> => {
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();
},
@@ -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<ResolveApiResponse>(
const res = await apiClient.post<ResolveApiResponse>(
"/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<Omit<PoLineItem, "qty">[]>(
const res = await apiClient.get<Omit<PoLineItem, "qty">[]>(
"/api/products/batch",
{ params: { pids: chunk.join(",") } }
);
@@ -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<CampaignsResponse>({
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()
},
@@ -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<Stats>({
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()
},
@@ -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<ScoreBreakdown>({
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<RecommendationResponse>({
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")
@@ -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<BestSellersData>({
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()
},
@@ -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<AccuracyData>({
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")
}
@@ -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}`);
@@ -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<OverstockMetricsData>({
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();
},
@@ -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<PurchaseMetricsData>({
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();
},
@@ -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<ReplenishmentMetricsData>({
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();
},
@@ -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()
},
@@ -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<StockMetricsData>({
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();
},
@@ -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<Product[]>({
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()
},
@@ -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<Product[]>({
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()
},
@@ -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) {
@@ -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 {
@@ -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];
@@ -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<boolean> | null = null;
async function ensureInitialized(): Promise<boolean> {
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 }),
@@ -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");
}
@@ -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<ReusableImage[]>({
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");
}
@@ -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',
@@ -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",
@@ -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 = <T extends string>({
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 = <T extends string>({
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 = <T extends string>({
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 = <T extends string>({
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 = <T extends string>({
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 = <T extends string>({
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',
@@ -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)}`
);
@@ -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({
@@ -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<AiValidationResponse> => {
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({
@@ -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 }),
@@ -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 }),
@@ -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<FieldOptionsResponse> => {
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');
}
@@ -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 }),
@@ -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<SelectOption[]> => {
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<SelectOption[]>
* Fetch sublines for a product line
*/
const fetchSublinesApi = async (lineId: string): Promise<SelectOption[]> => {
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 }) => ({
@@ -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 }),
@@ -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({
@@ -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<UpcValidationResult> => {
try {
const response = await fetch(
const response = await apiFetch(
`${config.apiUrl}/import/check-upc-and-generate-sku?upc=${encodeURIComponent(upcValue)}&supplierId=${encodeURIComponent(supplierId)}`
);
@@ -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, unknown>[]): 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');
}
@@ -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();
},
@@ -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();
},
@@ -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))
@@ -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}`);
@@ -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);
@@ -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:", {
@@ -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'
@@ -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'
});
@@ -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<AiPrompt[]>({
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<FieldOptions>({
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) {
@@ -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<ReusableImage[]>({
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<FieldOptions>({
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) {
@@ -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<Template[]>({
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<FieldOptions>({
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) {
@@ -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') {
@@ -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}`
@@ -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'
});
@@ -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
@@ -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'
+2 -1
View File
@@ -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<InventorySummary>({
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();
},
+2 -1
View File
@@ -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",
});
+6 -5
View File
@@ -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<TResponse, TSort extends string>(opts: UseAggregateLis
const query = useQuery<TResponse, Error>({
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<BrandStats, Error>({
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<FilterOptions, Error>({
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<VendorStats, Error>({
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<FilterOptions, Error>({
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();
},
+8 -6
View File
@@ -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<string, string> = { 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 }),
+4 -3
View File
@@ -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",
+3 -2
View File
@@ -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();
+3 -2
View File
@@ -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();
},
+2 -1
View File
@@ -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) {
+4 -3
View File
@@ -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");
}
+7 -6
View File
@@ -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<string, string> = { 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,
});
+5 -4
View File
@@ -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<LineProductsResponse>({
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<LineResponse, Error>({
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<LineStats, Error>({
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<LineFilterOptions, Error>({
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();
},
+4 -3
View File
@@ -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();
},
+7 -6
View File
@@ -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) {
+4 -3
View File
@@ -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");
+3 -2
View File
@@ -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) {
+2 -1
View File
@@ -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<void> {
try {
await fetch(BASE_URL, {
await apiFetch(BASE_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(entry),
+8 -7
View File
@@ -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<T>(response: Response): Promise<T> {
* List all sessions for a user (named + unnamed)
*/
export async function listSessions(userId: number): Promise<ImportSessionListItem[]> {
const response = await fetch(`${BASE_URL}?user_id=${userId}`);
const response = await apiFetch(`${BASE_URL}?user_id=${userId}`);
return handleResponse<ImportSessionListItem[]>(response);
}
@@ -44,7 +45,7 @@ export async function listSessions(userId: number): Promise<ImportSessionListIte
* Get a specific session by ID (includes full data)
*/
export async function getSession(id: number): Promise<ImportSession> {
const response = await fetch(`${BASE_URL}/${id}`);
const response = await apiFetch(`${BASE_URL}/${id}`);
return handleResponse<ImportSession>(response);
}
@@ -52,7 +53,7 @@ export async function getSession(id: number): Promise<ImportSession> {
* Create a new named session
*/
export async function createSession(data: ImportSessionCreateRequest): Promise<ImportSession> {
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<I
* Update an existing session by ID
*/
export async function updateSession(id: number, data: ImportSessionUpdateRequest): Promise<ImportSession> {
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<ImportSession> {
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<void> {
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<void> {
* Delete the unnamed/autosave session for a user
*/
export async function deleteAutosaveSession(userId: number): Promise<void> {
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
+4 -3
View File
@@ -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"
@@ -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<void> {
try {
await fetch(BASE_URL, {
await apiFetch(BASE_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(entry),
+35
View File
@@ -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<Response> {
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;
}
+30
View File
@@ -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;
File diff suppressed because one or more lines are too long