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 | | 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 | | | 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 | | 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) | | 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 | | 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) ## 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 ### The discovery
@@ -1,4 +1,5 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { apiFetch } from '@/utils/api';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { import {
ResponsiveContainer, ResponsiveContainer,
@@ -34,7 +35,7 @@ export function AgingSellThrough() {
const { data, isLoading, isError } = useQuery<AgingCohort[]>({ const { data, isLoading, isError } = useQuery<AgingCohort[]>({
queryKey: ['aging-sell-through'], queryKey: ['aging-sell-through'],
queryFn: async () => { 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'); if (!response.ok) throw new Error('Failed to fetch aging data');
return response.json(); return response.json();
}, },
@@ -1,4 +1,5 @@
import { useState } from 'react'; import { useState } from 'react';
import { apiFetch } from '@/utils/api';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { import {
@@ -45,7 +46,7 @@ export function CapitalEfficiency() {
const { data, isLoading, isError } = useQuery<EfficiencyData>({ const { data, isLoading, isError } = useQuery<EfficiencyData>({
queryKey: ['capital-efficiency'], queryKey: ['capital-efficiency'],
queryFn: async () => { 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'); if (!response.ok) throw new Error('Failed to fetch capital efficiency');
return response.json(); return response.json();
}, },
@@ -1,4 +1,5 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { apiFetch } from '@/utils/api';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { import {
ResponsiveContainer, ResponsiveContainer,
@@ -34,7 +35,7 @@ export function DiscountImpact() {
const { data, isLoading, isError } = useQuery<DiscountRow[]>({ const { data, isLoading, isError } = useQuery<DiscountRow[]>({
queryKey: ['discount-impact'], queryKey: ['discount-impact'],
queryFn: async () => { 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'); if (!response.ok) throw new Error('Failed to fetch discount data');
return response.json(); return response.json();
}, },
@@ -1,4 +1,5 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { apiFetch } from '@/utils/api';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { import {
ResponsiveContainer, ResponsiveContainer,
@@ -55,7 +56,7 @@ export function GrowthMomentum() {
const { data, isLoading, isError } = useQuery<GrowthData>({ const { data, isLoading, isError } = useQuery<GrowthData>({
queryKey: ['growth-momentum'], queryKey: ['growth-momentum'],
queryFn: async () => { 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'); if (!response.ok) throw new Error('Failed to fetch growth data');
return response.json(); return response.json();
}, },
@@ -1,4 +1,5 @@
import { useState, useMemo } from 'react'; import { useState, useMemo } from 'react';
import { apiFetch } from '@/utils/api';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { import {
@@ -39,7 +40,7 @@ export function InventoryFlow() {
const { data, isLoading, isError } = useQuery<FlowPoint[]>({ const { data, isLoading, isError } = useQuery<FlowPoint[]>({
queryKey: ['inventory-flow', period], queryKey: ['inventory-flow', period],
queryFn: async () => { 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'); if (!response.ok) throw new Error('Failed to fetch inventory flow');
return response.json(); return response.json();
}, },
@@ -1,4 +1,5 @@
import { useState } from 'react'; import { useState } from 'react';
import { apiFetch } from '@/utils/api';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { import {
@@ -35,7 +36,7 @@ export function InventoryTrends() {
const { data, isLoading, isError } = useQuery<TrendPoint[]>({ const { data, isLoading, isError } = useQuery<TrendPoint[]>({
queryKey: ['inventory-trends', period], queryKey: ['inventory-trends', period],
queryFn: async () => { 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'); if (!response.ok) throw new Error('Failed to fetch inventory trends');
return response.json(); return response.json();
}, },
@@ -1,4 +1,5 @@
import { useState } from 'react'; import { useState } from 'react';
import { apiFetch } from '@/utils/api';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { import {
@@ -36,7 +37,7 @@ export function InventoryValueTrend() {
const { data, isLoading, isError } = useQuery<ValuePoint[]>({ const { data, isLoading, isError } = useQuery<ValuePoint[]>({
queryKey: ['inventory-value', period], queryKey: ['inventory-value', period],
queryFn: async () => { 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'); if (!response.ok) throw new Error('Failed to fetch inventory value');
return response.json(); return response.json();
}, },
@@ -1,4 +1,5 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { apiFetch } from '@/utils/api';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { import {
ResponsiveContainer, ResponsiveContainer,
@@ -42,7 +43,7 @@ export function PortfolioAnalysis() {
const { data, isLoading, isError } = useQuery<PortfolioData>({ const { data, isLoading, isError } = useQuery<PortfolioData>({
queryKey: ['portfolio-analysis'], queryKey: ['portfolio-analysis'],
queryFn: async () => { 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'); if (!response.ok) throw new Error('Failed to fetch portfolio analysis');
return response.json(); return response.json();
}, },
@@ -1,4 +1,5 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { apiFetch } from '@/utils/api';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { import {
ResponsiveContainer, ResponsiveContainer,
@@ -54,7 +55,7 @@ export function SeasonalPatterns() {
const { data, isLoading, isError } = useQuery<SeasonalData>({ const { data, isLoading, isError } = useQuery<SeasonalData>({
queryKey: ['seasonal-patterns'], queryKey: ['seasonal-patterns'],
queryFn: async () => { 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'); if (!response.ok) throw new Error('Failed to fetch seasonal data');
return response.json(); return response.json();
}, },
@@ -1,4 +1,5 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { apiFetch } from '@/utils/api';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { import {
ResponsiveContainer, ResponsiveContainer,
@@ -72,7 +73,7 @@ export function StockHealth() {
const { data, isLoading, isError } = useQuery<StockHealthData>({ const { data, isLoading, isError } = useQuery<StockHealthData>({
queryKey: ['stock-health'], queryKey: ['stock-health'],
queryFn: async () => { 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'); if (!response.ok) throw new Error('Failed to fetch stock health');
return response.json(); return response.json();
}, },
@@ -1,4 +1,5 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { apiFetch } from '@/utils/api';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { import {
ResponsiveContainer, ResponsiveContainer,
@@ -49,7 +50,7 @@ export function StockoutRisk() {
const { data, isLoading, isError } = useQuery<StockoutRiskData>({ const { data, isLoading, isError } = useQuery<StockoutRiskData>({
queryKey: ['stockout-risk'], queryKey: ['stockout-risk'],
queryFn: async () => { 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'); if (!response.ok) throw new Error('Failed to fetch stockout risk');
return response.json(); return response.json();
}, },
+5 -4
View File
@@ -1,4 +1,5 @@
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef } from 'react';
import { apiFetch } from '@/utils/api';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
@@ -73,7 +74,7 @@ export function ChatRoom({ roomId, selectedUserId }: ChatRoomProps) {
const fetchRoom = async () => { const fetchRoom = async () => {
try { 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(); const data = await response.json();
if (data.status === 'success') { if (data.status === 'success') {
@@ -101,7 +102,7 @@ export function ChatRoom({ roomId, selectedUserId }: ChatRoomProps) {
params.set('before', before); 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(); const data = await response.json();
if (data.status === 'success') { if (data.status === 'success') {
@@ -136,7 +137,7 @@ export function ChatRoom({ roomId, selectedUserId }: ChatRoomProps) {
const loadAttachments = async (messageIds: number[]) => { const loadAttachments = async (messageIds: number[]) => {
try { try {
const response = await fetch(`${config.chatUrl}/messages/attachments`, { const response = await apiFetch(`${config.chatUrl}/messages/attachments`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -172,7 +173,7 @@ export function ChatRoom({ roomId, selectedUserId }: ChatRoomProps) {
if (!searchQuery || searchQuery.length < 2) return; if (!searchQuery || searchQuery.length < 2) return;
try { try {
const response = await fetch( const response = await apiFetch(
`${config.chatUrl}/users/${selectedUserId}/search?q=${encodeURIComponent(searchQuery)}&limit=20` `${config.chatUrl}/users/${selectedUserId}/search?q=${encodeURIComponent(searchQuery)}&limit=20`
); );
const data = await response.json(); const data = await response.json();
+2 -1
View File
@@ -1,4 +1,5 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { apiFetch } from '@/utils/api';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Loader2, Hash, Lock, Users, MessageSquare } from 'lucide-react'; import { Loader2, Hash, Lock, Users, MessageSquare } from 'lucide-react';
@@ -33,7 +34,7 @@ export function ChatTest({ selectedUserId }: ChatTestProps) {
setError(null); setError(null);
try { try {
const response = await fetch(`${config.chatUrl}/users/${selectedUserId}/rooms`); const response = await apiFetch(`${config.chatUrl}/users/${selectedUserId}/rooms`);
const data = await response.json(); const data = await response.json();
if (data.status === 'success') { if (data.status === 'success') {
+2 -1
View File
@@ -1,4 +1,5 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { apiFetch } from '@/utils/api';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Loader2, Hash, Users, MessageSquare, MessageCircle, Users2 } from 'lucide-react'; import { Loader2, Hash, Users, MessageSquare, MessageCircle, Users2 } from 'lucide-react';
@@ -50,7 +51,7 @@ export function RoomList({ selectedUserId, selectedRoomId, onRoomSelect }: RoomL
setError(null); setError(null);
try { try {
const response = await fetch(`${config.chatUrl}/users/${selectedUserId}/rooms`); const response = await apiFetch(`${config.chatUrl}/users/${selectedUserId}/rooms`);
const data = await response.json(); const data = await response.json();
if (data.status === 'success') { if (data.status === 'success') {
@@ -53,7 +53,7 @@ import {
X, X,
} from "lucide-react"; } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import axios from "axios"; import { apiClient } from "@/utils/apiClient";
import type { SearchProduct } from "@/components/product-editor/types"; import type { SearchProduct } from "@/components/product-editor/types";
import { import {
parsePastedTable, parsePastedTable,
@@ -165,7 +165,7 @@ export function AddProductsDialog({
setIsSearching(true); setIsSearching(true);
setSearched(true); setSearched(true);
try { try {
const res = await axios.get<{ results: QuickSearchResult[]; total: number }>( const res = await apiClient.get<{ results: QuickSearchResult[]; total: number }>(
"/api/products/search", "/api/products/search",
{ params: { q: query } } { params: { q: query } }
); );
@@ -182,7 +182,7 @@ export function AddProductsDialog({
async (result: QuickSearchResult) => { async (result: QuickSearchResult) => {
// Look up full details to pull MOQ (to default qty sensibly) // Look up full details to pull MOQ (to default qty sensibly)
try { 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 }, params: { pid: result.pid },
}); });
const full = (res.data ?? [])[0]; const full = (res.data ?? [])[0];
@@ -204,7 +204,7 @@ export function AddProductsDialog({
if (pids.length === 0) return; if (pids.length === 0) return;
try { 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(",") }, params: { pid: pids.join(",") },
}); });
const items = (res.data ?? []).map((p) => ({ const items = (res.data ?? []).map((p) => ({
@@ -9,6 +9,7 @@
*/ */
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { apiFetch } from '@/utils/api';
import { ComboboxField } from "@/components/product-editor/ComboboxField"; import { ComboboxField } from "@/components/product-editor/ComboboxField";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import type { FieldOption } from "@/components/product-editor/types"; import type { FieldOption } from "@/components/product-editor/types";
@@ -30,7 +31,7 @@ export function SupplierSelector({
const { data, isLoading, error } = useQuery({ const { data, isLoading, error } = useQuery({
queryKey: ["field-options"], queryKey: ["field-options"],
queryFn: async (): Promise<FieldOptionsResponse> => { 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"); if (!res.ok) throw new Error("Failed to load suppliers");
return res.json(); return res.json();
}, },
@@ -16,7 +16,7 @@
* full PoLineItem rows via `GET /api/products/batch`. * full PoLineItem rows via `GET /api/products/batch`.
*/ */
import axios from "axios"; import { apiClient } from "@/utils/apiClient";
import type { import type {
RawIdentifierRow, RawIdentifierRow,
ResolveResult, ResolveResult,
@@ -66,7 +66,7 @@ export async function resolveIdentifiers(
const identifiers = rows.map((r) => r.identifier); const identifiers = rows.map((r) => r.identifier);
const res = await axios.post<ResolveApiResponse>( const res = await apiClient.post<ResolveApiResponse>(
"/api/products/resolve-identifiers", "/api/products/resolve-identifiers",
{ identifiers } { identifiers }
); );
@@ -120,7 +120,7 @@ export async function fetchBatchProducts(
for (let i = 0; i < uniqPids.length; i += BATCH_LOOKUP_MAX_PIDS) { for (let i = 0; i < uniqPids.length; i += BATCH_LOOKUP_MAX_PIDS) {
const chunk = uniqPids.slice(i, 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", "/api/products/batch",
{ params: { pids: chunk.join(",") } } { params: { pids: chunk.join(",") } }
); );
@@ -1,4 +1,5 @@
import { useState, useMemo } from "react" import { useState, useMemo } from "react"
import { apiFetch } from '@/utils/api';
import { useQuery } from "@tanstack/react-query" import { useQuery } from "@tanstack/react-query"
import { import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger,
@@ -18,7 +19,7 @@ function useCampaignData(open: boolean) {
const campaigns = useQuery<CampaignsResponse>({ const campaigns = useQuery<CampaignsResponse>({
queryKey: ["newsletter-campaigns"], queryKey: ["newsletter-campaigns"],
queryFn: async () => { 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") if (!res.ok) throw new Error("Failed to fetch campaigns")
return res.json() return res.json()
}, },
@@ -29,7 +30,7 @@ function useCampaignData(open: boolean) {
const products = useQuery<{ products: ProductAggregate[] }>({ const products = useQuery<{ products: ProductAggregate[] }>({
queryKey: ["newsletter-campaigns-products"], queryKey: ["newsletter-campaigns-products"],
queryFn: async () => { 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") if (!res.ok) throw new Error("Failed to fetch")
return res.json() return res.json()
}, },
@@ -40,7 +41,7 @@ function useCampaignData(open: boolean) {
const links = useQuery<{ links: LinkAggregate[] }>({ const links = useQuery<{ links: LinkAggregate[] }>({
queryKey: ["newsletter-campaigns-links"], queryKey: ["newsletter-campaigns-links"],
queryFn: async () => { 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") if (!res.ok) throw new Error("Failed to fetch")
return res.json() return res.json()
}, },
@@ -51,7 +52,7 @@ function useCampaignData(open: boolean) {
const brands = useQuery<{ brands: BrandAggregate[] }>({ const brands = useQuery<{ brands: BrandAggregate[] }>({
queryKey: ["newsletter-campaigns-brands"], queryKey: ["newsletter-campaigns-brands"],
queryFn: async () => { 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") if (!res.ok) throw new Error("Failed to fetch")
return res.json() return res.json()
}, },
@@ -1,4 +1,5 @@
import { useQuery } from "@tanstack/react-query" import { useQuery } from "@tanstack/react-query"
import { apiFetch } from '@/utils/api';
import { Card, CardContent } from "@/components/ui/card" import { Card, CardContent } from "@/components/ui/card"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
import { Skeleton } from "@/components/ui/skeleton" import { Skeleton } from "@/components/ui/skeleton"
@@ -18,7 +19,7 @@ export function NewsletterStats() {
const { data } = useQuery<Stats>({ const { data } = useQuery<Stats>({
queryKey: ["newsletter-stats"], queryKey: ["newsletter-stats"],
queryFn: async () => { 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") if (!res.ok) throw new Error("Failed to fetch stats")
return res.json() return res.json()
}, },
@@ -1,4 +1,5 @@
import { useQuery } from "@tanstack/react-query" import { useQuery } from "@tanstack/react-query"
import { apiFetch } from '@/utils/api';
import { useState, useMemo, useContext } from "react" import { useState, useMemo, useContext } from "react"
import { import {
Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
@@ -170,7 +171,7 @@ function ScoreBreakdownTooltip({ pid, score, children }: { pid: number; score: n
const { data } = useQuery<ScoreBreakdown>({ const { data } = useQuery<ScoreBreakdown>({
queryKey: ["score-breakdown", pid], queryKey: ["score-breakdown", pid],
queryFn: async () => { 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") if (!res.ok) throw new Error("Failed")
return res.json() return res.json()
}, },
@@ -246,7 +247,7 @@ export function RecommendationTable({ category }: RecommendationTableProps) {
const { data, isLoading } = useQuery<RecommendationResponse>({ const { data, isLoading } = useQuery<RecommendationResponse>({
queryKey: ["newsletter-recommendations", category, page], queryKey: ["newsletter-recommendations", category, page],
queryFn: async () => { queryFn: async () => {
const res = await fetch( const res = await apiFetch(
`${config.apiUrl}/newsletter/recommendations?category=${category}&page=${page}&limit=${limit}` `${config.apiUrl}/newsletter/recommendations?category=${category}&page=${page}&limit=${limit}`
) )
if (!res.ok) throw new Error("Failed to fetch recommendations") if (!res.ok) throw new Error("Failed to fetch recommendations")
@@ -1,4 +1,5 @@
import { useQuery } from "@tanstack/react-query" import { useQuery } from "@tanstack/react-query"
import { apiFetch } from '@/utils/api';
import { CardHeader, CardTitle, CardContent } from "@/components/ui/card" import { CardHeader, CardTitle, CardContent } from "@/components/ui/card"
import { ScrollArea } from "@/components/ui/scroll-area" import { ScrollArea } from "@/components/ui/scroll-area"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
@@ -52,7 +53,7 @@ export function BestSellers() {
const { data, isError, isLoading } = useQuery<BestSellersData>({ const { data, isError, isLoading } = useQuery<BestSellersData>({
queryKey: ["best-sellers"], queryKey: ["best-sellers"],
queryFn: async () => { 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"); if (!response.ok) throw new Error("Failed to fetch best sellers");
return response.json() return response.json()
}, },
@@ -1,4 +1,5 @@
import { useQuery } from "@tanstack/react-query" 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 { BarChart, Bar, ResponsiveContainer, XAxis, YAxis, Tooltip as RechartsTooltip, Cell, LineChart, Line } from "recharts"
import config from "@/config" import config from "@/config"
import { Target, TrendingDown, ArrowUpDown } from "lucide-react" import { Target, TrendingDown, ArrowUpDown } from "lucide-react"
@@ -83,7 +84,7 @@ export function ForecastAccuracy() {
const { data, error, isLoading } = useQuery<AccuracyData>({ const { data, error, isLoading } = useQuery<AccuracyData>({
queryKey: ["forecast-accuracy"], queryKey: ["forecast-accuracy"],
queryFn: async () => { queryFn: async () => {
const response = await fetch(`${config.apiUrl}/dashboard/forecast/accuracy`) const response = await apiFetch(`${config.apiUrl}/dashboard/forecast/accuracy`)
if (!response.ok) { if (!response.ok) {
throw new Error("Failed to fetch forecast accuracy") throw new Error("Failed to fetch forecast accuracy")
} }
@@ -1,4 +1,5 @@
import { useQuery } from "@tanstack/react-query" import { useQuery } from "@tanstack/react-query"
import { apiFetch } from '@/utils/api';
import { CardHeader, CardTitle, CardContent } from "@/components/ui/card" import { CardHeader, CardTitle, CardContent } from "@/components/ui/card"
import { AreaChart, Area, ResponsiveContainer, XAxis, YAxis, Tooltip as RechartsTooltip } from "recharts" import { AreaChart, Area, ResponsiveContainer, XAxis, YAxis, Tooltip as RechartsTooltip } from "recharts"
import { useState } from "react" import { useState } from "react"
@@ -72,7 +73,7 @@ export function ForecastMetrics() {
startDate: new Date().toISOString(), startDate: new Date().toISOString(),
endDate: getEndDate(period).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) { if (!response.ok) {
const text = await response.text(); const text = await response.text();
throw new Error(`Failed to fetch forecast metrics: ${text}`); throw new Error(`Failed to fetch forecast metrics: ${text}`);
@@ -1,4 +1,5 @@
import { useQuery } from "@tanstack/react-query" import { useQuery } from "@tanstack/react-query"
import { apiFetch } from '@/utils/api';
import { CardHeader, CardTitle, CardContent } from "@/components/ui/card" import { CardHeader, CardTitle, CardContent } from "@/components/ui/card"
import config from "@/config" import config from "@/config"
import { formatCurrency } from "@/utils/formatCurrency" import { formatCurrency } from "@/utils/formatCurrency"
@@ -38,7 +39,7 @@ export function OverstockMetrics() {
const { data, isError, isLoading } = useQuery<OverstockMetricsData>({ const { data, isError, isLoading } = useQuery<OverstockMetricsData>({
queryKey: ["overstock-metrics"], queryKey: ["overstock-metrics"],
queryFn: async () => { 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'); if (!response.ok) throw new Error('Failed to fetch overstock metrics');
return response.json(); return response.json();
}, },
@@ -1,4 +1,5 @@
import { useQuery } from "@tanstack/react-query" import { useQuery } from "@tanstack/react-query"
import { apiFetch } from '@/utils/api';
import { CardHeader, CardTitle, CardContent } from "@/components/ui/card" import { CardHeader, CardTitle, CardContent } from "@/components/ui/card"
import { PieChart, Pie, ResponsiveContainer, Cell, Sector } from "recharts" import { PieChart, Pie, ResponsiveContainer, Cell, Sector } from "recharts"
import config from "@/config" import config from "@/config"
@@ -75,7 +76,7 @@ export function PurchaseMetrics() {
const { data, isError, isLoading } = useQuery<PurchaseMetricsData>({ const { data, isError, isLoading } = useQuery<PurchaseMetricsData>({
queryKey: ["purchase-metrics"], queryKey: ["purchase-metrics"],
queryFn: async () => { 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'); if (!response.ok) throw new Error('Failed to fetch purchase metrics');
return response.json(); return response.json();
}, },
@@ -1,4 +1,5 @@
import { useQuery } from "@tanstack/react-query" import { useQuery } from "@tanstack/react-query"
import { apiFetch } from '@/utils/api';
import { CardHeader, CardTitle, CardContent } from "@/components/ui/card" import { CardHeader, CardTitle, CardContent } from "@/components/ui/card"
import config from "@/config" import config from "@/config"
import { formatCurrency } from "@/utils/formatCurrency" import { formatCurrency } from "@/utils/formatCurrency"
@@ -40,7 +41,7 @@ export function ReplenishmentMetrics() {
const { data, isError, isLoading } = useQuery<ReplenishmentMetricsData>({ const { data, isError, isLoading } = useQuery<ReplenishmentMetricsData>({
queryKey: ["replenishment-metrics"], queryKey: ["replenishment-metrics"],
queryFn: async () => { 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'); if (!response.ok) throw new Error('Failed to fetch replenishment metrics');
return response.json(); return response.json();
}, },
@@ -1,4 +1,5 @@
import { useQuery } from "@tanstack/react-query" import { useQuery } from "@tanstack/react-query"
import { apiFetch } from '@/utils/api';
import { CardHeader, CardTitle, CardContent } from "@/components/ui/card" import { CardHeader, CardTitle, CardContent } from "@/components/ui/card"
import { AreaChart, Area, ResponsiveContainer, XAxis, YAxis, Tooltip as RechartsTooltip } from "recharts" import { AreaChart, Area, ResponsiveContainer, XAxis, YAxis, Tooltip as RechartsTooltip } from "recharts"
import { useState } from "react" import { useState } from "react"
@@ -61,7 +62,7 @@ export function SalesMetrics() {
startDate: addDays(new Date(), -period).toISOString(), startDate: addDays(new Date(), -period).toISOString(),
endDate: new Date().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"); if (!response.ok) throw new Error("Failed to fetch sales metrics");
return response.json() return response.json()
}, },
@@ -1,4 +1,5 @@
import { useQuery } from "@tanstack/react-query" import { useQuery } from "@tanstack/react-query"
import { apiFetch } from '@/utils/api';
import { CardHeader, CardTitle, CardContent } from "@/components/ui/card" import { CardHeader, CardTitle, CardContent } from "@/components/ui/card"
import { PieChart, Pie, ResponsiveContainer, Cell, Sector } from "recharts" import { PieChart, Pie, ResponsiveContainer, Cell, Sector } from "recharts"
import config from "@/config" import config from "@/config"
@@ -107,7 +108,7 @@ export function StockMetrics() {
const { data, isError, isLoading } = useQuery<StockMetricsData>({ const { data, isError, isLoading } = useQuery<StockMetricsData>({
queryKey: ["stock-metrics"], queryKey: ["stock-metrics"],
queryFn: async () => { 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'); if (!response.ok) throw new Error('Failed to fetch stock metrics');
return response.json(); return response.json();
}, },
@@ -1,4 +1,5 @@
import { useQuery } from "@tanstack/react-query" import { useQuery } from "@tanstack/react-query"
import { apiFetch } from '@/utils/api';
import { CardHeader, CardTitle, CardContent } from "@/components/ui/card" import { CardHeader, CardTitle, CardContent } from "@/components/ui/card"
import { ScrollArea } from "@/components/ui/scroll-area" import { ScrollArea } from "@/components/ui/scroll-area"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
@@ -29,7 +30,7 @@ export function TopOverstockedProducts() {
const { data, isError, isLoading } = useQuery<Product[]>({ const { data, isError, isLoading } = useQuery<Product[]>({
queryKey: ["top-overstocked-products"], queryKey: ["top-overstocked-products"],
queryFn: async () => { 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"); if (!response.ok) throw new Error("Failed to fetch overstocked products");
return response.json() return response.json()
}, },
@@ -1,4 +1,5 @@
import { useQuery } from "@tanstack/react-query" import { useQuery } from "@tanstack/react-query"
import { apiFetch } from '@/utils/api';
import { CardHeader, CardTitle, CardContent } from "@/components/ui/card" import { CardHeader, CardTitle, CardContent } from "@/components/ui/card"
import { ScrollArea } from "@/components/ui/scroll-area" import { ScrollArea } from "@/components/ui/scroll-area"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
@@ -29,7 +30,7 @@ export function TopReplenishProducts() {
const { data, isError, isLoading } = useQuery<Product[]>({ const { data, isError, isLoading } = useQuery<Product[]>({
queryKey: ["top-replenish-products"], queryKey: ["top-replenish-products"],
queryFn: async () => { 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"); if (!response.ok) throw new Error("Failed to fetch products to replenish");
return response.json() return response.json()
}, },
@@ -1,5 +1,5 @@
import { useState, useCallback, useRef } from "react"; import { useState, useCallback, useRef } from "react";
import axios from "axios"; import { apiClient } from "@/utils/apiClient";
import { toast } from "sonner"; import { toast } from "sonner";
import { useDropzone } from "react-dropzone"; import { useDropzone } from "react-dropzone";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
@@ -305,7 +305,7 @@ export function ImageManager({
try { try {
const formData = new FormData(); const formData = new FormData();
formData.append("image", file); 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) { if (res.data?.imageUrl) {
addNewImage(res.data.imageUrl); addNewImage(res.data.imageUrl);
} }
@@ -356,7 +356,7 @@ export function ImageManager({
setAddOpen(false); setAddOpen(false);
try { try {
const res = await axios.post("/api/import/upload-image-url", { const res = await apiClient.post("/api/import/upload-image-url", {
imageUrl: normalizeImageUrlInput(url), imageUrl: normalizeImageUrlInput(url),
}); });
if (res.data?.imageUrl) { if (res.data?.imageUrl) {
@@ -1,5 +1,6 @@
import { useState, useEffect, useCallback, useRef, useContext } from "react"; import { useState, useEffect, useCallback, useRef, useContext } from "react";
import axios from "axios"; import axios from "axios";
import { apiClient } from "@/utils/apiClient";
import { useForm, Controller } from "react-hook-form"; import { useForm, Controller } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -499,7 +500,7 @@ export function ProductEditForm({
originalValuesRef.current = { ...data }; originalValuesRef.current = { ...data };
if (imageChanges) { if (imageChanges) {
try { 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; originalImagesRef.current = res.data;
setProductImages(res.data); setProductImages(res.data);
} catch { } catch {
@@ -1,5 +1,5 @@
import { useState, useCallback } from "react"; import { useState, useCallback } from "react";
import axios from "axios"; import { apiClient } from "@/utils/apiClient";
import { toast } from "sonner"; import { toast } from "sonner";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -57,11 +57,11 @@ export function ProductSearch({
setIsSearching(true); setIsSearching(true);
onNewSearch(); onNewSearch();
try { try {
const res = await axios.get("/api/products/search", { const res = await apiClient.get("/api/products/search", {
params: { q: searchTerm }, params: { q: searchTerm },
}); });
if (res.data.total === 0) { 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 }, params: { q: searchTerm },
}); });
const fullProducts = fallback.data as SearchProduct[]; const fullProducts = fallback.data as SearchProduct[];
@@ -102,7 +102,7 @@ export function ProductSearch({
} }
setIsLoadingProduct(product.pid); setIsLoadingProduct(product.pid);
try { try {
const res = await axios.get("/api/import/search-products", { const res = await apiClient.get("/api/import/search-products", {
params: { pid: product.pid }, params: { pid: product.pid },
}); });
const full = (res.data as SearchProduct[])[0]; const full = (res.data as SearchProduct[])[0];
@@ -11,6 +11,7 @@
*/ */
import { useState, useRef, useCallback, useEffect } from 'react'; import { useState, useRef, useCallback, useEffect } from 'react';
import { apiFetch } from '@/utils/api';
import type { TaxonomySuggestion, ProductSuggestions } from '@/components/product-import/steps/ValidationStep/store/types'; import type { TaxonomySuggestion, ProductSuggestions } from '@/components/product-import/steps/ValidationStep/store/types';
const API_BASE = '/api/ai'; const API_BASE = '/api/ai';
@@ -20,7 +21,7 @@ let initPromise: Promise<boolean> | null = null;
async function ensureInitialized(): Promise<boolean> { async function ensureInitialized(): Promise<boolean> {
if (!initPromise) { if (!initPromise) {
initPromise = fetch(`${API_BASE}/initialize`, { method: 'POST' }) initPromise = apiFetch(`${API_BASE}/initialize`, { method: 'POST' })
.then((r) => r.json()) .then((r) => r.json())
.then((d) => Boolean(d.success)) .then((d) => Boolean(d.success))
.catch(() => { .catch(() => {
@@ -81,7 +82,7 @@ export function useProductSuggestions(product: ProductInput): ProductSuggestionR
return; return;
} }
const response = await fetch(`${API_BASE}/suggestions`, { const response = await apiFetch(`${API_BASE}/suggestions`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ product: p }), body: JSON.stringify({ product: p }),
@@ -1,4 +1,5 @@
import { useCallback, useContext, useEffect, useMemo, useState } from "react"; import { useCallback, useContext, useEffect, useMemo, useState } from "react";
import { apiFetch } from '@/utils/api';
import { Check, ChevronsUpDown, Loader2 } from "lucide-react"; import { Check, ChevronsUpDown, Loader2 } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -137,7 +138,7 @@ export function CreateProductCategoryDialog({
setIsLoadingLines(true); setIsLoadingLines(true);
try { try {
const response = await fetch(`${config.apiUrl}/import/product-lines/${targetCompanyId}`); const response = await apiFetch(`${config.apiUrl}/import/product-lines/${targetCompanyId}`);
if (!response.ok) { if (!response.ok) {
throw new Error("Failed to load product lines"); throw new Error("Failed to load product lines");
} }
@@ -1,4 +1,5 @@
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { apiFetch } from '@/utils/api';
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { Loader2, Link as LinkIcon, Image as ImageIcon } from "lucide-react"; 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[]>({ const { data: reusableImages, isLoading: isLoadingReusable } = useQuery<ReusableImage[]>({
queryKey: ["reusable-images"], queryKey: ["reusable-images"],
queryFn: async () => { queryFn: async () => {
const response = await fetch(`${config.apiUrl}/reusable-images`); const response = await apiFetch(`${config.apiUrl}/reusable-images`);
if (!response.ok) { if (!response.ok) {
throw new Error("Failed to fetch reusable images"); throw new Error("Failed to fetch reusable images");
} }
@@ -1,4 +1,5 @@
import { toast } from "sonner"; import { toast } from "sonner";
import { apiFetch } from '@/utils/api';
import config from "@/config"; import config from "@/config";
import { Product, ProductImageSortable, ImageMetadata, ImageNotice } from "../types"; import { Product, ProductImageSortable, ImageMetadata, ImageNotice } from "../types";
@@ -248,7 +249,7 @@ export const useProductImageOperations = ({
try { try {
// Upload the image // Upload the image
const response = await fetch(`${config.apiUrl}/import/upload-image`, { const response = await apiFetch(`${config.apiUrl}/import/upload-image`, {
method: 'POST', method: 'POST',
body: formData, body: formData,
}); });
@@ -355,7 +356,7 @@ export const useProductImageOperations = ({
const filename = urlParts[urlParts.length - 1]; const filename = urlParts[urlParts.length - 1];
// Call API to delete the image // 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', method: 'DELETE',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -1,4 +1,5 @@
import { useState } from "react"; import { useState } from "react";
import { apiFetch } from '@/utils/api';
import { toast } from "sonner"; import { toast } from "sonner";
import config from "@/config"; import config from "@/config";
import { Product, ProductImageSortable } from "../types"; import { Product, ProductImageSortable } from "../types";
@@ -59,7 +60,7 @@ export const useUrlImageUpload = ({
let fileName = "From URL"; let fileName = "From URL";
if (isLikelyWebpUrl(validatedUrl)) { 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", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -1,4 +1,5 @@
import React, { useCallback, useEffect, useMemo, useState, memo, useRef, useLayoutEffect } from "react" import React, { useCallback, useEffect, useMemo, useState, memo, useRef, useLayoutEffect } from "react"
import { apiFetch } from '@/utils/api';
import { useRsi } from "../../hooks/useRsi" import { useRsi } from "../../hooks/useRsi"
import { setColumn } from "./utils/setColumn" import { setColumn } from "./utils/setColumn"
import { setIgnoreColumn } from "./utils/setIgnoreColumn" import { setIgnoreColumn } from "./utils/setIgnoreColumn"
@@ -672,7 +673,7 @@ const MatchColumnsStepComponent = <T extends string>({
queryKey: ["product-lines", globalSelections.company], queryKey: ["product-lines", globalSelections.company],
queryFn: async () => { queryFn: async () => {
if (!globalSelections.company) return []; 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) { if (!response.ok) {
throw new Error("Failed to fetch product lines"); throw new Error("Failed to fetch product lines");
} }
@@ -687,7 +688,7 @@ const MatchColumnsStepComponent = <T extends string>({
queryKey: ["sublines", globalSelections.line], queryKey: ["sublines", globalSelections.line],
queryFn: async () => { queryFn: async () => {
if (!globalSelections.line) return []; 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) { if (!response.ok) {
throw new Error("Failed to fetch sublines"); throw new Error("Failed to fetch sublines");
} }
@@ -756,7 +757,7 @@ const MatchColumnsStepComponent = <T extends string>({
queryKey: ["product-lines-mapped", mappedCompanyValue], queryKey: ["product-lines-mapped", mappedCompanyValue],
queryFn: async () => { queryFn: async () => {
if (!mappedCompanyValue) return []; 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) { if (!response.ok) {
throw new Error("Failed to fetch product lines for mapped company"); throw new Error("Failed to fetch product lines for mapped company");
} }
@@ -771,7 +772,7 @@ const MatchColumnsStepComponent = <T extends string>({
queryKey: ["sublines-mapped", mappedLineValue], queryKey: ["sublines-mapped", mappedLineValue],
queryFn: async () => { queryFn: async () => {
if (!mappedLineValue) return []; if (!mappedLineValue) return [];
const response = await fetch(`${config.apiUrl}/import/sublines/${mappedLineValue}`); const response = await apiFetch(`${config.apiUrl}/import/sublines/${mappedLineValue}`);
if (!response.ok) { if (!response.ok) {
throw new Error("Failed to fetch sublines for mapped line"); throw new Error("Failed to fetch sublines for mapped line");
} }
@@ -785,7 +786,7 @@ const MatchColumnsStepComponent = <T extends string>({
const { data: fieldOptionsData } = useQuery({ const { data: fieldOptionsData } = useQuery({
queryKey: ["field-options"], queryKey: ["field-options"],
queryFn: async () => { queryFn: async () => {
const response = await fetch(`${config.apiUrl}/import/field-options`); const response = await apiFetch(`${config.apiUrl}/import/field-options`);
if (!response.ok) { if (!response.ok) {
throw new Error("Failed to fetch field options"); throw new Error("Failed to fetch field options");
} }
@@ -852,7 +853,7 @@ const MatchColumnsStepComponent = <T extends string>({
setIsRefreshing(true); setIsRefreshing(true);
try { try {
// Clear backend cache // 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', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -13,6 +13,7 @@
*/ */
import { useMemo, useRef, useCallback, memo, useState } from 'react'; import { useMemo, useRef, useCallback, memo, useState } from 'react';
import { apiFetch } from '@/utils/api';
import { type ColumnDef } from '@tanstack/react-table'; import { type ColumnDef } from '@tanstack/react-table';
import { useVirtualizer } from '@tanstack/react-virtual'; import { useVirtualizer } from '@tanstack/react-virtual';
import { Checkbox } from '@/components/ui/checkbox'; import { Checkbox } from '@/components/ui/checkbox';
@@ -249,7 +250,7 @@ const CellWrapper = memo(({
setIsGeneratingUpc(true); setIsGeneratingUpc(true);
try { try {
const response = await fetch(`${config.apiUrl}/import/generate-upc`, { const response = await apiFetch(`${config.apiUrl}/import/generate-upc`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ supplierId: supplierIdString }), body: JSON.stringify({ supplierId: supplierIdString }),
@@ -287,7 +288,7 @@ const CellWrapper = memo(({
startValidatingCell(rowIndex, 'item_number'); startValidatingCell(rowIndex, 'item_number');
try { try {
const validationResponse = await fetch( const validationResponse = await apiFetch(
`${config.apiUrl}/import/check-upc-and-generate-sku?upc=${encodeURIComponent(upc)}&supplierId=${encodeURIComponent(supplierIdString)}` `${config.apiUrl}/import/check-upc-and-generate-sku?upc=${encodeURIComponent(upc)}&supplierId=${encodeURIComponent(supplierIdString)}`
); );
@@ -491,7 +492,7 @@ const CellWrapper = memo(({
if (!cached) { if (!cached) {
// Start loading state and fetch product lines // Start loading state and fetch product lines
state.setLoadingProductLines(companyId, true); state.setLoadingProductLines(companyId, true);
fetch(`/api/import/product-lines/${companyId}`) apiFetch(`/api/import/product-lines/${companyId}`)
.then(res => res.json()) .then(res => res.json())
.then(lines => { .then(lines => {
const opts = lines.map((line: { name?: string; label?: string; value?: string | number; id?: string | number }) => ({ const opts = lines.map((line: { name?: string; label?: string; value?: string | number; id?: string | number }) => ({
@@ -523,7 +524,7 @@ const CellWrapper = memo(({
if (!cached) { if (!cached) {
// Start loading state and fetch sublines // Start loading state and fetch sublines
state.setLoadingSublines(lineId, true); state.setLoadingSublines(lineId, true);
fetch(`/api/import/sublines/${lineId}`) apiFetch(`/api/import/sublines/${lineId}`)
.then(res => res.json()) .then(res => res.json())
.then(sublines => { .then(sublines => {
const opts = sublines.map((subline: { name?: string; label?: string; value?: string | number; id?: string | number }) => ({ 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, line: valueToSave as string | number,
}); });
fetch('/api/ai/validate/inline/name', { apiFetch('/api/ai/validate/inline/name', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ product: payload }), body: JSON.stringify({ product: payload }),
@@ -596,7 +597,7 @@ const CellWrapper = memo(({
const payload = buildDescriptionValidationPayload(currentRowForContext, fields); const payload = buildDescriptionValidationPayload(currentRowForContext, fields);
fetch('/api/ai/validate/inline/description', { apiFetch('/api/ai/validate/inline/description', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ product: payload }), body: JSON.stringify({ product: payload }),
@@ -640,7 +641,7 @@ const CellWrapper = memo(({
startValidatingCell(rowIndex, 'item_number'); startValidatingCell(rowIndex, 'item_number');
try { try {
const response = await fetch( const response = await apiFetch(
`${config.apiUrl}/import/check-upc-and-generate-sku?upc=${encodeURIComponent(upc)}&supplierId=${encodeURIComponent(supplierId)}` `${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/name'
: '/api/ai/validate/inline/description'; : '/api/ai/validate/inline/description';
fetch(endpoint, { apiFetch(endpoint, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ product: productPayload }), body: JSON.stringify({ product: productPayload }),
@@ -793,7 +794,7 @@ const CellWrapper = memo(({
? '/api/ai/validate/inline/name' ? '/api/ai/validate/inline/name'
: '/api/ai/validate/inline/description'; : '/api/ai/validate/inline/description';
fetch(endpoint, { apiFetch(endpoint, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ product: payload }), body: JSON.stringify({ product: payload }),
@@ -824,7 +825,7 @@ const CellWrapper = memo(({
state.setLoadingProductLines(companyId, true); state.setLoadingProductLines(companyId, true);
try { 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 lines = await response.json();
const opts = lines.map((ln: { name?: string; label?: string; value?: string | number; id?: string | number }) => ({ 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), label: ln.name || ln.label || String(ln.value || ln.id),
@@ -847,7 +848,7 @@ const CellWrapper = memo(({
state.setLoadingSublines(lineId, true); state.setLoadingSublines(lineId, true);
try { try {
const response = await fetch(`/api/import/sublines/${lineId}`); const response = await apiFetch(`/api/import/sublines/${lineId}`);
const sublines = await response.json(); const sublines = await response.json();
const opts = sublines.map((subline: { name?: string; label?: string; value?: string | number; id?: string | number }) => ({ 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), 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); const payload = buildNameValidationPayload(updatedRow, fields, rows);
fetch('/api/ai/validate/inline/name', { apiFetch('/api/ai/validate/inline/name', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ product: payload }), body: JSON.stringify({ product: payload }),
@@ -1164,7 +1165,7 @@ const TemplateCell = memo(({ rowIndex, currentTemplateId, defaultBrand }: Templa
const payload = buildDescriptionValidationPayload(updatedRow, fields); const payload = buildDescriptionValidationPayload(updatedRow, fields);
fetch('/api/ai/validate/inline/description', { apiFetch('/api/ai/validate/inline/description', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ product: payload }), body: JSON.stringify({ product: payload }),
@@ -1206,7 +1207,7 @@ const TemplateCell = memo(({ rowIndex, currentTemplateId, defaultBrand }: Templa
startValidatingCell(rowIndex, 'item_number'); startValidatingCell(rowIndex, 'item_number');
try { try {
const response = await fetch( const response = await apiFetch(
`${config.apiUrl}/import/check-upc-and-generate-sku?upc=${encodeURIComponent(upc)}&supplierId=${encodeURIComponent(supplierId)}` `${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 React, { createContext, useContext, useRef, useCallback, useEffect, useState } from 'react';
import { apiFetch } from '@/utils/api';
import type { RowData, ProductSuggestions, TaxonomySuggestion } from '../store/types'; import type { RowData, ProductSuggestions, TaxonomySuggestion } from '../store/types';
// ============================================================================ // ============================================================================
@@ -104,7 +105,7 @@ export function AiSuggestionsProvider({
if (isInitialized) return true; if (isInitialized) return true;
try { try {
const response = await fetch(`${API_BASE}/initialize`, { method: 'POST' }); const response = await apiFetch(`${API_BASE}/initialize`, { method: 'POST' });
const data = await response.json(); const data = await response.json();
if (!response.ok || !data.success) { if (!response.ok || !data.success) {
@@ -157,7 +158,7 @@ export function AiSuggestionsProvider({
notifySubscribers(productIndex); notifySubscribers(productIndex);
try { try {
const response = await fetch(`${API_BASE}/suggestions`, { const response = await apiFetch(`${API_BASE}/suggestions`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ product: productData }), body: JSON.stringify({ product: productData }),
@@ -214,7 +215,7 @@ export function AiSuggestionsProvider({
}); });
try { try {
const response = await fetch(`${API_BASE}/suggestions/batch`, { const response = await apiFetch(`${API_BASE}/suggestions/batch`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
@@ -5,6 +5,7 @@
*/ */
import config from '@/config'; import config from '@/config';
import { apiFetch } from '@/utils/api';
import type { RowData } from '../../store/types'; import type { RowData } from '../../store/types';
import type { Field } from '../../../../types'; import type { Field } from '../../../../types';
import { prepareDataForAiValidation } from '../../utils/aiValidationUtils'; import { prepareDataForAiValidation } from '../../utils/aiValidationUtils';
@@ -164,7 +165,7 @@ export const runAiValidation = async (
request: AiValidationRequest request: AiValidationRequest
): Promise<AiValidationResponse> => { ): Promise<AiValidationResponse> => {
try { try {
const response = await fetch(`${config.apiUrl}/ai-validation/validate`, { const response = await apiFetch(`${config.apiUrl}/ai-validation/validate`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(request), body: JSON.stringify(request),
@@ -201,7 +202,7 @@ export const getAiDebugPrompt = async (
// For time estimation before validation, send all products // For time estimation before validation, send all products
const productsToSend = options?.previewOnly ? products.slice(0, 5) : 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', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
@@ -14,6 +14,7 @@
*/ */
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
import { apiFetch } from '@/utils/api';
import { useValidationStore } from '../store/validationStore'; import { useValidationStore } from '../store/validationStore';
import { useInitPhase } from '../store/selectors'; import { useInitPhase } from '../store/selectors';
import { import {
@@ -48,7 +49,7 @@ async function triggerValidation(
: '/api/ai/validate/inline/description'; : '/api/ai/validate/inline/description';
try { try {
const response = await fetch(endpoint, { const response = await apiFetch(endpoint, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ product: payload }), body: JSON.stringify({ product: payload }),
@@ -10,6 +10,7 @@
*/ */
import { useEffect } from 'react'; import { useEffect } from 'react';
import { apiFetch } from '@/utils/api';
import { useValidationStore } from '../store/validationStore'; import { useValidationStore } from '../store/validationStore';
import { useUpcValidation } from './useUpcValidation'; import { useUpcValidation } from './useUpcValidation';
import type { Field } from '../../../types'; import type { Field } from '../../../types';
@@ -47,7 +48,7 @@ async function triggerInlineAiValidation(
: '/api/ai/validate/inline/description'; : '/api/ai/validate/inline/description';
try { try {
const response = await fetch(endpoint, { const response = await apiFetch(endpoint, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ product: productPayload }), body: JSON.stringify({ product: productPayload }),
@@ -6,6 +6,7 @@
*/ */
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { apiFetch } from '@/utils/api';
import config from '@/config'; import config from '@/config';
import type { SelectOption } from '../../../types'; import type { SelectOption } from '../../../types';
@@ -25,7 +26,7 @@ export interface FieldOptionsResponse {
* Fetch field options from the API * Fetch field options from the API
*/ */
const fetchFieldOptions = async (): Promise<FieldOptionsResponse> => { 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) { if (!response.ok) {
throw new Error('Failed to fetch field options'); throw new Error('Failed to fetch field options');
} }
@@ -6,6 +6,7 @@
*/ */
import { useState, useCallback, useRef } from 'react'; import { useState, useCallback, useRef } from 'react';
import { apiFetch } from '@/utils/api';
// Types for the validation results // Types for the validation results
export interface InlineAiResult { export interface InlineAiResult {
@@ -73,7 +74,7 @@ export function useInlineAiValidation() {
setState(prev => ({ ...prev, isValidating: true, error: null })); setState(prev => ({ ...prev, isValidating: true, error: null }));
try { try {
const response = await fetch('/api/ai/validate/inline/name', { const response = await apiFetch('/api/ai/validate/inline/name', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ product }), body: JSON.stringify({ product }),
@@ -138,7 +139,7 @@ export function useInlineAiValidation() {
setState(prev => ({ ...prev, isValidating: true, error: null })); setState(prev => ({ ...prev, isValidating: true, error: null }));
try { try {
const response = await fetch('/api/ai/validate/inline/description', { const response = await apiFetch('/api/ai/validate/inline/description', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ product }), body: JSON.stringify({ product }),
@@ -12,13 +12,13 @@
import { useCallback, useRef } from 'react'; import { useCallback, useRef } from 'react';
import { useValidationStore } from '../store/validationStore'; import { useValidationStore } from '../store/validationStore';
import type { SelectOption } from '../../../types'; import type { SelectOption } from '../../../types';
import axios from 'axios'; import { apiClient } from "@/utils/apiClient";
/** /**
* Fetch product lines for a company * Fetch product lines for a company
*/ */
const fetchProductLinesApi = async (companyId: string): Promise<SelectOption[]> => { 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; const lines = response.data;
return lines.map((line: { name?: string; label?: string; value?: string | number; id?: string | number }) => ({ 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 * Fetch sublines for a product line
*/ */
const fetchSublinesApi = async (lineId: string): Promise<SelectOption[]> => { 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; const sublines = response.data;
return sublines.map((subline: { name?: string; label?: string; value?: string | number; id?: string | number }) => ({ 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 { useState, useCallback, useRef } from 'react';
import { apiFetch } from '@/utils/api';
// Types for sanity check results // Types for sanity check results
export interface SanityIssue { export interface SanityIssue {
@@ -103,7 +104,7 @@ export function useSanityCheck() {
})); }));
try { try {
const response = await fetch('/api/ai/validate/sanity-check', { const response = await apiFetch('/api/ai/validate/sanity-check', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ products }), body: JSON.stringify({ products }),
@@ -10,6 +10,7 @@
*/ */
import { useCallback } from 'react'; import { useCallback } from 'react';
import { apiFetch } from '@/utils/api';
import { useValidationStore } from '../store/validationStore'; import { useValidationStore } from '../store/validationStore';
import { import {
useTemplates, useTemplates,
@@ -56,7 +57,7 @@ export const useTemplateManagement = () => {
setTemplatesLoading(true); setTemplatesLoading(true);
try { 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'); if (!response.ok) throw new Error('Failed to fetch templates');
const data = await response.json(); const data = await response.json();
@@ -162,7 +163,7 @@ export const useTemplateManagement = () => {
if (supplierId && upcValue) { if (supplierId && upcValue) {
// Don't await - let it run in background // Don't await - let it run in background
// UPC validation uses its own hooks which will update the store // 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((res) => res.json())
.then((result) => { .then((result) => {
if (result.itemNumber) { if (result.itemNumber) {
@@ -236,7 +237,7 @@ export const useTemplateManagement = () => {
} }
try { try {
const response = await fetch(`${config.apiUrl}/templates`, { const response = await apiFetch(`${config.apiUrl}/templates`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
@@ -10,6 +10,7 @@
*/ */
import { useCallback, useRef } from 'react'; import { useCallback, useRef } from 'react';
import { apiFetch } from '@/utils/api';
import { useValidationStore } from '../store/validationStore'; import { useValidationStore } from '../store/validationStore';
import { useInitialUpcValidationDone } from '../store/selectors'; import { useInitialUpcValidationDone } from '../store/selectors';
import { ErrorSource, ErrorType, type UpcValidationResult } from '../store/types'; import { ErrorSource, ErrorType, type UpcValidationResult } from '../store/types';
@@ -27,7 +28,7 @@ const fetchProductByUpc = async (
upcValue: string upcValue: string
): Promise<UpcValidationResult> => { ): Promise<UpcValidationResult> => {
try { try {
const response = await fetch( const response = await apiFetch(
`${config.apiUrl}/import/check-upc-and-generate-sku?upc=${encodeURIComponent(upcValue)}&supplierId=${encodeURIComponent(supplierId)}` `${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 { useEffect, useRef, useDeferredValue, useMemo } from 'react';
import { apiFetch } from '@/utils/api';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useValidationStore } from './store/validationStore'; import { useValidationStore } from './store/validationStore';
import { useInitPhase, useIsReady } from './store/selectors'; import { useInitPhase, useIsReady } from './store/selectors';
@@ -58,7 +59,7 @@ const createDataFingerprint = (data: Record<string, unknown>[]): string => {
* Fetch field options from the API * Fetch field options from the API
*/ */
const fetchFieldOptions = async () => { const fetchFieldOptions = async () => {
const response = await fetch(`${config.apiUrl}/import/field-options`); const response = await apiFetch(`${config.apiUrl}/import/field-options`);
if (!response.ok) { if (!response.ok) {
throw new Error('Failed to fetch field options'); throw new Error('Failed to fetch field options');
} }
@@ -1,4 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import { apiFetch } from '@/utils/api';
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { Drawer as VaulDrawer } from "vaul"; import { Drawer as VaulDrawer } from "vaul";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
@@ -77,7 +78,7 @@ export function ProductDetail({ productId, onClose }: ProductDetailProps) {
queryKey: ["productMetricDetail", productId], queryKey: ["productMetricDetail", productId],
queryFn: async () => { queryFn: async () => {
if (!productId) throw new Error("Product ID is required"); 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) { if (!response.ok) {
const errorData = await response.json().catch(() => ({})); const errorData = await response.json().catch(() => ({}));
throw new Error(`Failed to fetch product details (${response.status}): ${errorData.error || 'Server error'}`); 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], queryKey: ["productTimeSeries", productId],
queryFn: async () => { queryFn: async () => {
if (!productId) throw new Error("Product ID is required"); 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) { if (!response.ok) {
const errorData = await response.json().catch(() => ({})); const errorData = await response.json().catch(() => ({}));
throw new Error(`Failed to fetch time series data (${response.status}): ${errorData.error || 'Server error'}`); 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], queryKey: ["productForecast", productId],
queryFn: async () => { queryFn: async () => {
if (!productId) throw new Error("Product ID is required"); 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"); if (!response.ok) throw new Error("Failed to fetch forecast");
return response.json(); return response.json();
}, },
@@ -1,4 +1,5 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { apiFetch } from '@/utils/api';
import { import {
DollarSign, DollarSign,
Package, Package,
@@ -282,7 +283,7 @@ export function ProductSummaryCards({ activeView, showNonReplenishable, showInvi
const params = new URLSearchParams(); const params = new URLSearchParams();
if (showNonReplenishable) params.append('showNonReplenishable', 'true'); if (showNonReplenishable) params.append('showNonReplenishable', 'true');
if (showInvisible) params.append('showInvisible', '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'); if (!response.ok) throw new Error('Failed to fetch summary');
return response.json(); return response.json();
}, },
@@ -1,4 +1,5 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { apiFetch } from '@/utils/api';
import { import {
ResponsiveContainer, ResponsiveContainer,
BarChart, BarChart,
@@ -40,7 +41,7 @@ export default function PipelineCard() {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
useEffect(() => { useEffect(() => {
fetch('/api/purchase-orders/pipeline') apiFetch('/api/purchase-orders/pipeline')
.then(res => res.ok ? res.json() : Promise.reject('Failed')) .then(res => res.ok ? res.json() : Promise.reject('Failed'))
.then(setData) .then(setData)
.catch(err => console.error('Pipeline fetch error:', err)) .catch(err => console.error('Pipeline fetch error:', err))
@@ -1,4 +1,5 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { apiFetch } from '@/utils/api';
import { import {
Table, Table,
TableBody, TableBody,
@@ -85,7 +86,7 @@ export default function PurchaseOrderAccordion({
? `/api/purchase-orders/receiving/${purchaseOrder.id}/items` ? `/api/purchase-orders/receiving/${purchaseOrder.id}/items`
: `/api/purchase-orders/${purchaseOrder.id}/items`; : `/api/purchase-orders/${purchaseOrder.id}/items`;
const response = await fetch(endpoint); const response = await apiFetch(endpoint);
if (!response.ok) { if (!response.ok) {
throw new Error(`Failed to fetch items: ${response.statusText}`); throw new Error(`Failed to fetch items: ${response.statusText}`);
@@ -1,5 +1,5 @@
import { useState, useEffect, useCallback } from "react"; 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 { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -92,7 +92,7 @@ export function AuditLog() {
if (filterPid.trim()) params.pid = filterPid.trim(); if (filterPid.trim()) params.pid = filterPid.trim();
if (filterAction !== "all") params.action = filterAction; if (filterAction !== "all") params.action = filterAction;
} }
const res = await axios.get(baseUrl, { params }); const res = await apiClient.get(baseUrl, { params });
setEntries(res.data.entries); setEntries(res.data.entries);
setTotal(res.data.total); setTotal(res.data.total);
} catch { } catch {
@@ -125,7 +125,7 @@ export function AuditLog() {
const viewDetail = async (id: number) => { const viewDetail = async (id: number) => {
setIsLoadingDetail(true); setIsLoadingDetail(true);
try { try {
const res = await axios.get(`${baseUrl}/${id}`); const res = await apiClient.get(`${baseUrl}/${id}`);
setDetail(res.data); setDetail(res.data);
} catch { } catch {
setDetail(null); setDetail(null);
@@ -1,4 +1,5 @@
import { useState, useEffect, useRef } from "react"; import { useState, useEffect, useRef } from "react";
import { apiFetch } from '@/utils/api';
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Card, Card,
@@ -189,7 +190,7 @@ export function DataManagement() {
const checkActiveProcess = async () => { const checkActiveProcess = async () => {
try { try {
console.log("Checking for active processes..."); console.log("Checking for active processes...");
const response = await fetch(`${config.apiUrl}/csv/status`, { const response = await apiFetch(`${config.apiUrl}/csv/status`, {
credentials: "include", credentials: "include",
}); });
@@ -509,7 +510,7 @@ export function DataManagement() {
// Connect to the update SSE endpoint // Connect to the update SSE endpoint
connectToEventSource("update"); connectToEventSource("update");
const response = await fetch(`${config.apiUrl}/csv/full-update`, { const response = await apiFetch(`${config.apiUrl}/csv/full-update`, {
method: "POST", method: "POST",
credentials: "include", credentials: "include",
}); });
@@ -543,7 +544,7 @@ export function DataManagement() {
// Connect to the reset SSE endpoint // Connect to the reset SSE endpoint
connectToEventSource("reset"); connectToEventSource("reset");
const response = await fetch(`${config.apiUrl}/csv/full-reset`, { const response = await apiFetch(`${config.apiUrl}/csv/full-reset`, {
method: "POST", method: "POST",
credentials: "include", credentials: "include",
}); });
@@ -572,7 +573,7 @@ export function DataManagement() {
// Determine which operation is running // Determine which operation is running
const type = isUpdating ? "update" : "reset"; 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", method: "POST",
credentials: "include", credentials: "include",
}); });
@@ -613,11 +614,11 @@ export function DataManagement() {
console.log("Fetching history data..."); console.log("Fetching history data...");
const [importRes, calcRes, moduleRes, tableRes, tableCountsRes] = await Promise.all([ const [importRes, calcRes, moduleRes, tableRes, tableCountsRes] = await Promise.all([
fetch(`${config.apiUrl}/csv/history/import`, { credentials: 'include' }), apiFetch(`${config.apiUrl}/csv/history/import`, { credentials: 'include' }),
fetch(`${config.apiUrl}/csv/history/calculate`, { credentials: 'include' }), apiFetch(`${config.apiUrl}/csv/history/calculate`, { credentials: 'include' }),
fetch(`${config.apiUrl}/csv/status/modules`, { credentials: 'include' }), apiFetch(`${config.apiUrl}/csv/status/modules`, { credentials: 'include' }),
fetch(`${config.apiUrl}/csv/status/tables`, { credentials: 'include' }), apiFetch(`${config.apiUrl}/csv/status/tables`, { credentials: 'include' }),
fetch(`${config.apiUrl}/csv/status/table-counts`, { credentials: 'include' }), apiFetch(`${config.apiUrl}/csv/status/table-counts`, { credentials: 'include' }),
]); ]);
console.log("Fetch responses:", { console.log("Fetch responses:", {
@@ -1,4 +1,5 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { apiFetch } from '@/utils/api';
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
@@ -25,7 +26,7 @@ export function GlobalSettings() {
const loadSettings = async () => { const loadSettings = async () => {
try { try {
setLoading(true); setLoading(true);
const response = await fetch(`${config.apiUrl}/config/global`, { const response = await apiFetch(`${config.apiUrl}/config/global`, {
credentials: 'include' credentials: 'include'
}); });
if (!response.ok) { if (!response.ok) {
@@ -49,7 +50,7 @@ export function GlobalSettings() {
const handleSaveSettings = async () => { const handleSaveSettings = async () => {
try { try {
const response = await fetch(`${config.apiUrl}/config/global`, { const response = await apiFetch(`${config.apiUrl}/config/global`, {
method: 'PUT', method: 'PUT',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
@@ -1,4 +1,5 @@
import { useState, useEffect, useCallback, useMemo } from 'react'; import { useState, useEffect, useCallback, useMemo } from 'react';
import { apiFetch } from '@/utils/api';
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
@@ -43,7 +44,7 @@ export function ProductSettings() {
const loadSettings = useCallback(async () => { const loadSettings = useCallback(async () => {
try { try {
setLoading(true); 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' credentials: 'include'
}); });
if (!response.ok) { if (!response.ok) {
@@ -75,7 +76,7 @@ export function ProductSettings() {
const setting = settings.find(s => s.pid === pid); const setting = settings.find(s => s.pid === pid);
if (!setting) return; if (!setting) return;
const response = await fetch(`${config.apiUrl}/config/products/${pid}`, { const response = await apiFetch(`${config.apiUrl}/config/products/${pid}`, {
method: 'PUT', method: 'PUT',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
@@ -104,7 +105,7 @@ export function ProductSettings() {
const handleResetToDefault = useCallback(async (pid: string) => { const handleResetToDefault = useCallback(async (pid: string) => {
try { try {
const response = await fetch(`${config.apiUrl}/config/products/${pid}/reset`, { const response = await apiFetch(`${config.apiUrl}/config/products/${pid}/reset`, {
method: 'POST', method: 'POST',
credentials: 'include' credentials: 'include'
}); });
@@ -1,4 +1,5 @@
import { useState, useMemo } from "react"; import { useState, useMemo } from "react";
import { apiFetch } from '@/utils/api';
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@@ -148,7 +149,7 @@ export function PromptManagement() {
const { data: prompts, isLoading } = useQuery<AiPrompt[]>({ const { data: prompts, isLoading } = useQuery<AiPrompt[]>({
queryKey: ["ai-prompts"], queryKey: ["ai-prompts"],
queryFn: async () => { queryFn: async () => {
const response = await fetch(`${config.apiUrl}/ai-prompts`); const response = await apiFetch(`${config.apiUrl}/ai-prompts`);
if (!response.ok) { if (!response.ok) {
throw new Error("Failed to fetch AI prompts"); throw new Error("Failed to fetch AI prompts");
} }
@@ -159,7 +160,7 @@ export function PromptManagement() {
const { data: fieldOptions } = useQuery<FieldOptions>({ const { data: fieldOptions } = useQuery<FieldOptions>({
queryKey: ["fieldOptions"], queryKey: ["fieldOptions"],
queryFn: async () => { queryFn: async () => {
const response = await fetch(`${config.apiUrl}/import/field-options`); const response = await apiFetch(`${config.apiUrl}/import/field-options`);
if (!response.ok) { if (!response.ok) {
throw new Error("Failed to fetch field options"); throw new Error("Failed to fetch field options");
} }
@@ -194,7 +195,7 @@ export function PromptManagement() {
const createMutation = useMutation({ const createMutation = useMutation({
mutationFn: async (data: { prompt_text: string; prompt_type: string; company: string | null; is_singleton: boolean }) => { 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", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(data), body: JSON.stringify(data),
@@ -220,7 +221,7 @@ export function PromptManagement() {
const updateMutation = useMutation({ const updateMutation = useMutation({
mutationFn: async (data: { id: number; prompt_text: string; prompt_type: string; company: string | null; is_singleton: boolean }) => { 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", method: "PUT",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(data), body: JSON.stringify(data),
@@ -248,7 +249,7 @@ export function PromptManagement() {
const deleteMutation = useMutation({ const deleteMutation = useMutation({
mutationFn: async (id: number) => { mutationFn: async (id: number) => {
const response = await fetch(`${config.apiUrl}/ai-prompts/${id}`, { const response = await apiFetch(`${config.apiUrl}/ai-prompts/${id}`, {
method: "DELETE", method: "DELETE",
}); });
if (!response.ok) { if (!response.ok) {
@@ -1,4 +1,5 @@
import { useState, useMemo, useCallback, useEffect } from "react"; import { useState, useMemo, useCallback, useEffect } from "react";
import { apiFetch } from '@/utils/api';
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@@ -235,7 +236,7 @@ export function ReusableImageManagement() {
const { data: images, isLoading } = useQuery<ReusableImage[]>({ const { data: images, isLoading } = useQuery<ReusableImage[]>({
queryKey: ["reusable-images"], queryKey: ["reusable-images"],
queryFn: async () => { queryFn: async () => {
const response = await fetch(`${config.apiUrl}/reusable-images`); const response = await apiFetch(`${config.apiUrl}/reusable-images`);
if (!response.ok) { if (!response.ok) {
throw new Error("Failed to fetch reusable images"); throw new Error("Failed to fetch reusable images");
} }
@@ -246,7 +247,7 @@ export function ReusableImageManagement() {
const { data: fieldOptions } = useQuery<FieldOptions>({ const { data: fieldOptions } = useQuery<FieldOptions>({
queryKey: ["fieldOptions"], queryKey: ["fieldOptions"],
queryFn: async () => { queryFn: async () => {
const response = await fetch(`${config.apiUrl}/import/field-options`); const response = await apiFetch(`${config.apiUrl}/import/field-options`);
if (!response.ok) { if (!response.ok) {
throw new Error("Failed to fetch field options"); throw new Error("Failed to fetch field options");
} }
@@ -271,7 +272,7 @@ export function ReusableImageManagement() {
throw new Error("Image file is required"); 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", method: "POST",
body: formData, body: formData,
}); });
@@ -297,7 +298,7 @@ export function ReusableImageManagement() {
mutationFn: async (data: ImageFormData) => { mutationFn: async (data: ImageFormData) => {
if (!data.id) throw new Error("Image ID is required for update"); 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", method: "PUT",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -328,7 +329,7 @@ export function ReusableImageManagement() {
const deleteMutation = useMutation({ const deleteMutation = useMutation({
mutationFn: async (id: number) => { mutationFn: async (id: number) => {
const response = await fetch(`${config.apiUrl}/reusable-images/${id}`, { const response = await apiFetch(`${config.apiUrl}/reusable-images/${id}`, {
method: "DELETE", method: "DELETE",
}); });
if (!response.ok) { if (!response.ok) {
@@ -1,4 +1,5 @@
import { useState, useMemo } from "react"; import { useState, useMemo } from "react";
import { apiFetch } from '@/utils/api';
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { TemplateForm } from "@/components/templates/TemplateForm"; import { TemplateForm } from "@/components/templates/TemplateForm";
@@ -91,7 +92,7 @@ export function TemplateManagement() {
const { data: templates, isLoading } = useQuery<Template[]>({ const { data: templates, isLoading } = useQuery<Template[]>({
queryKey: ["templates"], queryKey: ["templates"],
queryFn: async () => { queryFn: async () => {
const response = await fetch(`${config.apiUrl}/templates`); const response = await apiFetch(`${config.apiUrl}/templates`);
if (!response.ok) { if (!response.ok) {
throw new Error("Failed to fetch templates"); throw new Error("Failed to fetch templates");
} }
@@ -102,7 +103,7 @@ export function TemplateManagement() {
const { data: fieldOptions } = useQuery<FieldOptions>({ const { data: fieldOptions } = useQuery<FieldOptions>({
queryKey: ["fieldOptions"], queryKey: ["fieldOptions"],
queryFn: async () => { queryFn: async () => {
const response = await fetch(`${config.apiUrl}/import/field-options`); const response = await apiFetch(`${config.apiUrl}/import/field-options`);
if (!response.ok) { if (!response.ok) {
throw new Error("Failed to fetch field options"); throw new Error("Failed to fetch field options");
} }
@@ -112,7 +113,7 @@ export function TemplateManagement() {
const deleteMutation = useMutation({ const deleteMutation = useMutation({
mutationFn: async (id: number) => { mutationFn: async (id: number) => {
const response = await fetch(`${config.apiUrl}/templates/${id}`, { const response = await apiFetch(`${config.apiUrl}/templates/${id}`, {
method: "DELETE", method: "DELETE",
}); });
if (!response.ok) { if (!response.ok) {
@@ -1,4 +1,5 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { apiFetch } from '@/utils/api';
import { z } from "zod"; import { z } from "zod";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
@@ -122,7 +123,7 @@ export function UserForm({ user, permissions, onSave, onCancel }: UserFormProps)
useEffect(() => { useEffect(() => {
const fetchRocketChatUsers = async () => { const fetchRocketChatUsers = async () => {
try { try {
const response = await fetch(`${config.chatUrl}/users`); const response = await apiFetch(`${config.chatUrl}/users`);
const data = await response.json(); const data = await response.json();
if (data.status === 'success') { if (data.status === 'success') {
@@ -1,4 +1,5 @@
import { useState, useEffect, useContext } from "react"; import { useState, useEffect, useContext } from "react";
import { apiFetch } from '@/utils/api';
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -56,7 +57,7 @@ export function UserManagement() {
setError(null); setError(null);
// Fetch users // Fetch users
const usersResponse = await fetch(`${config.authUrl}/users`, { const usersResponse = await apiFetch(`${config.authUrl}/users`, {
headers: { headers: {
'Authorization': `Bearer ${token}` 'Authorization': `Bearer ${token}`
} }
@@ -82,7 +83,7 @@ export function UserManagement() {
setUsers(usersData); setUsers(usersData);
// Fetch permissions // Fetch permissions
const permissionsResponse = await fetch(`${config.authUrl}/permissions/categories`, { const permissionsResponse = await apiFetch(`${config.authUrl}/permissions/categories`, {
headers: { headers: {
'Authorization': `Bearer ${token}` 'Authorization': `Bearer ${token}`
} }
@@ -128,7 +129,7 @@ export function UserManagement() {
const handleEditUser = async (userId: number) => { const handleEditUser = async (userId: number) => {
try { try {
const response = await fetch(`${config.authUrl}/users/${userId}`, { const response = await apiFetch(`${config.authUrl}/users/${userId}`, {
headers: { headers: {
'Authorization': `Bearer ${token}` 'Authorization': `Bearer ${token}`
} }
@@ -216,7 +217,7 @@ export function UserManagement() {
console.log(`${method} request to ${endpoint}`); console.log(`${method} request to ${endpoint}`);
const response = await fetch(endpoint, { const response = await apiFetch(endpoint, {
method, method,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -264,7 +265,7 @@ export function UserManagement() {
try { try {
setLoading(true); setLoading(true);
const response = await fetch(`${config.authUrl}/users/${userId}`, { const response = await apiFetch(`${config.authUrl}/users/${userId}`, {
method: 'DELETE', method: 'DELETE',
headers: { headers: {
'Authorization': `Bearer ${token}` 'Authorization': `Bearer ${token}`
@@ -1,4 +1,5 @@
import { useState, useEffect, useCallback, useMemo } from 'react'; import { useState, useEffect, useCallback, useMemo } from 'react';
import { apiFetch } from '@/utils/api';
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
@@ -39,7 +40,7 @@ export function VendorSettings() {
const loadSettings = useCallback(async () => { const loadSettings = useCallback(async () => {
try { try {
setLoading(true); 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' credentials: 'include'
}); });
if (!response.ok) { if (!response.ok) {
@@ -75,7 +76,7 @@ export function VendorSettings() {
const setting = settings.find(s => s.vendor === vendor); const setting = settings.find(s => s.vendor === vendor);
if (!setting) return; 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', method: 'PUT',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
@@ -108,7 +109,7 @@ export function VendorSettings() {
const handleResetToDefault = useCallback(async (vendor: string) => { const handleResetToDefault = useCallback(async (vendor: string) => {
try { 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', method: 'POST',
credentials: 'include' credentials: 'include'
}); });
@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react'; 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 { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
@@ -342,7 +342,7 @@ export function SearchProductTemplateDialog({ isOpen, onClose, onTemplateCreated
apiParams.q = '*'; 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) => ({ setSearchResults(response.data.map((product: Product) => ({
...product, ...product,
@@ -408,7 +408,7 @@ export function SearchProductTemplateDialog({ isOpen, onClose, onTemplateCreated
const fetchFieldOptions = async () => { const fetchFieldOptions = async () => {
try { try {
const response = await axios.get('/api/import/field-options'); const response = await apiClient.get('/api/import/field-options');
setFieldOptions(response.data); setFieldOptions(response.data);
} catch (error) { } catch (error) {
console.error('Error fetching field options:', 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 // Fetch product categories if pid is available
if (product.pid) { if (product.pid) {
try { 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; const productCategories = response.data;
// Update the selected product with the categories // Update the selected product with the categories
@@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button'; 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 { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from '@/components/ui/dialog';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import { Loader2, ChevronsUpDown, Check } from 'lucide-react'; import { Loader2, ChevronsUpDown, Check } from 'lucide-react';
@@ -230,7 +230,7 @@ export function TemplateForm({
console.log('Sending request to:', endpoint, 'with data:', cleanFormData); console.log('Sending request to:', endpoint, 'with data:', cleanFormData);
await axios[method](endpoint, cleanFormData); await apiClient[method](endpoint, cleanFormData);
toast.success( toast.success(
mode === 'edit' ? 'Template updated successfully' : 'Template created successfully' mode === 'edit' ? 'Template updated successfully' : 'Template created successfully'
+2 -1
View File
@@ -1,4 +1,5 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { apiFetch } from '@/utils/api';
import { Card, CardContent, CardHeader, CardTitle } from '../components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '../components/ui/card';
import { InventoryValueTrend } from '../components/analytics/InventoryValueTrend'; import { InventoryValueTrend } from '../components/analytics/InventoryValueTrend';
import { InventoryFlow } from '../components/analytics/InventoryFlow'; import { InventoryFlow } from '../components/analytics/InventoryFlow';
@@ -31,7 +32,7 @@ export function Analytics() {
const { data: summary, isLoading, isError } = useQuery<InventorySummary>({ const { data: summary, isLoading, isError } = useQuery<InventorySummary>({
queryKey: ['inventory-summary'], queryKey: ['inventory-summary'],
queryFn: async () => { 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'); if (!response.ok) throw new Error('Failed to fetch inventory summary');
return response.json(); return response.json();
}, },
+2 -1
View File
@@ -1,4 +1,5 @@
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { apiFetch } from '@/utils/api';
import { import {
Card, Card,
CardContent, CardContent,
@@ -655,7 +656,7 @@ export function BlackFridayDashboard() {
const fetchRealtime = async () => { const fetchRealtime = async () => {
try { try {
const response = await fetch("/api/dashboard-analytics/realtime/basic", { const response = await apiFetch("/api/dashboard-analytics/realtime/basic", {
credentials: "include", credentials: "include",
}); });
+6 -5
View File
@@ -1,4 +1,5 @@
import { useState, useMemo, useCallback, type ReactNode } from "react"; import { useState, useMemo, useCallback, type ReactNode } from "react";
import { apiFetch } from '@/utils/api';
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; 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>({ const query = useQuery<TResponse, Error>({
queryKey: [opts.queryKeyPrefix, queryParams.toString()], queryKey: [opts.queryKeyPrefix, queryParams.toString()],
queryFn: async () => { queryFn: async () => {
const response = await fetch(`${config.apiUrl}/${opts.endpoint}?${queryParams.toString()}`, { const response = await apiFetch(`${config.apiUrl}/${opts.endpoint}?${queryParams.toString()}`, {
credentials: "include", credentials: "include",
}); });
if (!response.ok) throw new Error(`Network response was not ok (${response.status})`); 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>({ const { data: statsData, isLoading: isLoadingStats } = useQuery<BrandStats, Error>({
queryKey: ["brandsStats"], queryKey: ["brandsStats"],
queryFn: async () => { 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"); if (!response.ok) throw new Error("Failed to fetch brand stats");
return response.json(); return response.json();
}, },
@@ -429,7 +430,7 @@ function BrandsPanel() {
const { data: filterOptions } = useQuery<FilterOptions, Error>({ const { data: filterOptions } = useQuery<FilterOptions, Error>({
queryKey: ["brandsFilterOptions"], queryKey: ["brandsFilterOptions"],
queryFn: async () => { 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"); if (!response.ok) throw new Error("Failed to fetch filter options");
return response.json(); return response.json();
}, },
@@ -619,7 +620,7 @@ function VendorsPanel() {
const { data: statsData, isLoading: isLoadingStats } = useQuery<VendorStats, Error>({ const { data: statsData, isLoading: isLoadingStats } = useQuery<VendorStats, Error>({
queryKey: ["vendorsStats"], queryKey: ["vendorsStats"],
queryFn: async () => { 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"); if (!response.ok) throw new Error("Failed to fetch vendor stats");
return response.json(); return response.json();
}, },
@@ -628,7 +629,7 @@ function VendorsPanel() {
const { data: filterOptions } = useQuery<FilterOptions, Error>({ const { data: filterOptions } = useQuery<FilterOptions, Error>({
queryKey: ["vendorsFilterOptions"], queryKey: ["vendorsFilterOptions"],
queryFn: async () => { 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"); if (!response.ok) throw new Error("Failed to fetch filter options");
return response.json(); return response.json();
}, },
+8 -6
View File
@@ -1,5 +1,7 @@
import { useState, useEffect, useCallback, useMemo, useRef } from "react"; import { useState, useEffect, useCallback, useMemo, useRef } from "react";
import { apiFetch } from '@/utils/api';
import axios from "axios"; import axios from "axios";
import { apiClient } from "@/utils/apiClient";
import { toast } from "sonner"; import { toast } from "sonner";
import { Loader2, Sparkles, Save } from "lucide-react"; import { Loader2, Sparkles, Save } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -169,7 +171,7 @@ export default function BulkEdit() {
const hadExisting = allProducts.length > 0; const hadExisting = allProducts.length > 0;
setIsLoadingProducts(true); setIsLoadingProducts(true);
try { try {
const res = await axios.get("/api/import/search-products", { const res = await apiClient.get("/api/import/search-products", {
params: { pid: pids.join(",") }, params: { pid: pids.join(",") },
}); });
const fetched = res.data as SearchProduct[]; const fetched = res.data as SearchProduct[];
@@ -200,7 +202,7 @@ export default function BulkEdit() {
setAllProducts([]); setAllProducts([]);
setIsLoadingProducts(true); setIsLoadingProducts(true);
try { 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); setAllProducts(res.data);
setPage(1); setPage(1);
toast.success(`Loaded ${res.data.length} ${label} products`); toast.success(`Loaded ${res.data.length} ${label} products`);
@@ -215,7 +217,7 @@ export default function BulkEdit() {
if (landingExtras[tabKey]) return; if (landingExtras[tabKey]) return;
setIsLoadingExtras(true); setIsLoadingExtras(true);
try { try {
const res = await axios.get("/api/import/landing-extras", { const res = await apiClient.get("/api/import/landing-extras", {
params: { catId, sid: 0 }, params: { catId, sid: 0 },
}); });
setLandingExtras((prev) => ({ ...prev, [tabKey]: res.data })); setLandingExtras((prev) => ({ ...prev, [tabKey]: res.data }));
@@ -234,7 +236,7 @@ export default function BulkEdit() {
setAllProducts([]); setAllProducts([]);
setIsLoadingProducts(true); setIsLoadingProducts(true);
try { try {
const res = await axios.get("/api/import/path-products", { const res = await apiClient.get("/api/import/path-products", {
params: { path: extra.path }, params: { path: extra.path },
signal: controller.signal, signal: controller.signal,
}); });
@@ -279,7 +281,7 @@ export default function BulkEdit() {
try { try {
const params: Record<string, string> = { company: lineCompany, line: lineLine }; const params: Record<string, string> = { company: lineCompany, line: lineLine };
if (lineSubline) params.subline = lineSubline; 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); setAllProducts(res.data);
setPage(1); setPage(1);
toast.success(`Loaded ${res.data.length} products`); toast.success(`Loaded ${res.data.length} products`);
@@ -378,7 +380,7 @@ export default function BulkEdit() {
} }
try { try {
const response = await fetch(endpoint, { const response = await apiFetch(endpoint, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ product: payload }), body: JSON.stringify({ product: payload }),
+4 -3
View File
@@ -1,4 +1,5 @@
import { useState, useMemo, useCallback, useEffect } from "react"; import { useState, useMemo, useCallback, useEffect } from "react";
import { apiFetch } from '@/utils/api';
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { import {
@@ -392,7 +393,7 @@ export function Categories() {
const url = `${config.apiUrl}/categories-aggregate?${queryParams.toString()}`; const url = `${config.apiUrl}/categories-aggregate?${queryParams.toString()}`;
console.log("Fetching categories from URL:", url); console.log("Fetching categories from URL:", url);
const response = await fetch(url, { const response = await apiFetch(url, {
credentials: "include", credentials: "include",
headers: { headers: {
Accept: "application/json", Accept: "application/json",
@@ -427,7 +428,7 @@ export function Categories() {
>({ >({
queryKey: ["categoriesStats"], queryKey: ["categoriesStats"],
queryFn: async () => { queryFn: async () => {
const response = await fetch( const response = await apiFetch(
`${config.apiUrl}/categories-aggregate/stats`, `${config.apiUrl}/categories-aggregate/stats`,
{ {
credentials: "include", credentials: "include",
@@ -444,7 +445,7 @@ export function Categories() {
>({ >({
queryKey: ["categoriesFilterOptions"], queryKey: ["categoriesFilterOptions"],
queryFn: async () => { queryFn: async () => {
const response = await fetch( const response = await apiFetch(
`${config.apiUrl}/categories-aggregate/filter-options`, `${config.apiUrl}/categories-aggregate/filter-options`,
{ {
credentials: "include", credentials: "include",
+3 -2
View File
@@ -1,4 +1,5 @@
import React, { useState, useEffect, useContext } from 'react'; import React, { useState, useEffect, useContext } from 'react';
import { apiFetch } from '@/utils/api';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
@@ -63,7 +64,7 @@ export function Chat() {
useEffect(() => { useEffect(() => {
const fetchUsers = async () => { const fetchUsers = async () => {
try { try {
const response = await fetch(`${config.chatUrl}/users`); const response = await apiFetch(`${config.chatUrl}/users`);
const data = await response.json(); const data = await response.json();
if (data.status === 'success') { if (data.status === 'success') {
@@ -123,7 +124,7 @@ export function Chat() {
setSearching(true); setSearching(true);
try { try {
const response = await fetch( const response = await apiFetch(
`${config.chatUrl}/users/${selectedUserId}/search?q=${encodeURIComponent(globalSearchQuery)}&limit=20` `${config.chatUrl}/users/${selectedUserId}/search?q=${encodeURIComponent(globalSearchQuery)}&limit=20`
); );
const data = await response.json(); const data = await response.json();
+3 -2
View File
@@ -1,4 +1,5 @@
import { useEffect, useState, useMemo, Fragment } from "react"; import { useEffect, useState, useMemo, Fragment } from "react";
import { apiFetch } from '@/utils/api';
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useQuery, useQueryClient } from "@tanstack/react-query";
@@ -110,7 +111,7 @@ export default function Forecasting() {
const { data: brands = [], isLoading: brandsLoading } = useQuery({ const { data: brands = [], isLoading: brandsLoading } = useQuery({
queryKey: ["brands"], queryKey: ["brands"],
queryFn: async () => { 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"); if (!response.ok) throw new Error("Failed to fetch brands");
const data = await response.json(); const data = await response.json();
return Array.isArray(data) ? data : []; return Array.isArray(data) ? data : [];
@@ -128,7 +129,7 @@ export default function Forecasting() {
startDate: dateRange.from?.toISOString() || "", startDate: dateRange.from?.toISOString() || "",
endDate: dateRange.to?.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"); if (!response.ok) throw new Error("Failed to fetch forecast data");
return response.json(); return response.json();
}, },
+2 -1
View File
@@ -1,4 +1,5 @@
import { useEffect, useMemo, useRef, useState, type FormEvent, type MouseEvent } from "react"; import { useEffect, useMemo, useRef, useState, type FormEvent, type MouseEvent } from "react";
import { apiFetch } from '@/utils/api';
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { Search, Loader2, PackageOpen, Copy, Check } from "lucide-react"; import { Search, Loader2, PackageOpen, Copy, Check } from "lucide-react";
@@ -63,7 +64,7 @@ export default function HtsLookup() {
enabled: false, enabled: false,
queryFn: async () => { queryFn: async () => {
const params = new URLSearchParams({ search: submittedTerm }); 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(() => ({})); const payload = await response.json().catch(() => ({}));
if (!response.ok) { if (!response.ok) {
+4 -3
View File
@@ -1,4 +1,5 @@
import { useState, useContext, useMemo } from "react"; import { useState, useContext, useMemo } from "react";
import { apiFetch } from '@/utils/api';
import { ReactSpreadsheetImport, StepType } from "@/components/product-import"; import { ReactSpreadsheetImport, StepType } from "@/components/product-import";
import type { StepState } from "@/components/product-import/steps/UploadFlow"; import type { StepState } from "@/components/product-import/steps/UploadFlow";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -276,7 +277,7 @@ export function Import() {
const { data: fieldOptions, isLoading: isLoadingOptions } = useQuery({ const { data: fieldOptions, isLoading: isLoadingOptions } = useQuery({
queryKey: ["import-field-options"], queryKey: ["import-field-options"],
queryFn: async () => { queryFn: async () => {
const response = await fetch(`${config.apiUrl}/import/field-options`); const response = await apiFetch(`${config.apiUrl}/import/field-options`);
if (!response.ok) { if (!response.ok) {
throw new Error("Failed to fetch field options"); throw new Error("Failed to fetch field options");
} }
@@ -289,7 +290,7 @@ export function Import() {
queryKey: ["product-lines", selectedCompany], queryKey: ["product-lines", selectedCompany],
queryFn: async () => { queryFn: async () => {
if (!selectedCompany) return []; 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) { if (!response.ok) {
throw new Error("Failed to fetch product lines"); throw new Error("Failed to fetch product lines");
} }
@@ -303,7 +304,7 @@ export function Import() {
queryKey: ["sublines", selectedLine], queryKey: ["sublines", selectedLine],
queryFn: async () => { queryFn: async () => {
if (!selectedLine) return []; if (!selectedLine) return [];
const response = await fetch(`${config.apiUrl}/import/sublines/${selectedLine}`); const response = await apiFetch(`${config.apiUrl}/import/sublines/${selectedLine}`);
if (!response.ok) { if (!response.ok) {
throw new Error("Failed to fetch sublines"); throw new Error("Failed to fetch sublines");
} }
+7 -6
View File
@@ -1,5 +1,6 @@
import { useState, useEffect, useCallback, useMemo, useRef } from "react"; import { useState, useEffect, useCallback, useMemo, useRef } from "react";
import axios from "axios"; import axios from "axios";
import { apiClient } from "@/utils/apiClient";
import { toast } from "sonner"; import { toast } from "sonner";
import { Loader2, RefreshCw } from "lucide-react"; import { Loader2, RefreshCw } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -284,7 +285,7 @@ export default function ProductEditor() {
const hadExisting = allProducts.length > 0; const hadExisting = allProducts.length > 0;
setIsLoadingProducts(true); setIsLoadingProducts(true);
try { try {
const res = await axios.get("/api/import/search-products", { const res = await apiClient.get("/api/import/search-products", {
params: { pid: pids.join(",") }, params: { pid: pids.join(",") },
}); });
const fetched = res.data as SearchProduct[]; const fetched = res.data as SearchProduct[];
@@ -319,7 +320,7 @@ export default function ProductEditor() {
setAllProducts([]); setAllProducts([]);
setIsLoadingProducts(true); setIsLoadingProducts(true);
try { 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); setAllProducts(res.data);
setPage(1); setPage(1);
toast.success(`Loaded ${res.data.length} ${label} products`); toast.success(`Loaded ${res.data.length} ${label} products`);
@@ -334,7 +335,7 @@ export default function ProductEditor() {
if (landingExtras[tabKey]) return; if (landingExtras[tabKey]) return;
setIsLoadingExtras(true); setIsLoadingExtras(true);
try { try {
const res = await axios.get("/api/import/landing-extras", { const res = await apiClient.get("/api/import/landing-extras", {
params: { catId, sid: 0 }, params: { catId, sid: 0 },
}); });
setLandingExtras((prev) => ({ ...prev, [tabKey]: res.data })); setLandingExtras((prev) => ({ ...prev, [tabKey]: res.data }));
@@ -356,7 +357,7 @@ export default function ProductEditor() {
setAllProducts([]); setAllProducts([]);
setIsLoadingProducts(true); setIsLoadingProducts(true);
try { try {
const res = await axios.get("/api/import/path-products", { const res = await apiClient.get("/api/import/path-products", {
params: { path: extra.path }, params: { path: extra.path },
signal: controller.signal, signal: controller.signal,
}); });
@@ -411,7 +412,7 @@ export default function ProductEditor() {
try { try {
const params: Record<string, string> = { company: lineCompany, line: lineLine }; const params: Record<string, string> = { company: lineCompany, line: lineLine };
if (lineSubline) params.subline = lineSubline; 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); setAllProducts(res.data);
setPage(1); setPage(1);
toast.success(`Loaded ${res.data.length} products`); toast.success(`Loaded ${res.data.length} products`);
@@ -432,7 +433,7 @@ export default function ProductEditor() {
setQueryStatus(null); setQueryStatus(null);
setIsLoadingProducts(true); setIsLoadingProducts(true);
try { try {
const res = await axios.get("/api/import/query-products", { const res = await apiClient.get("/api/import/query-products", {
params: { query_id: qid }, params: { query_id: qid },
signal: controller.signal, signal: controller.signal,
}); });
+5 -4
View File
@@ -1,4 +1,5 @@
import { useState, useMemo, useCallback } from "react"; import { useState, useMemo, useCallback } from "react";
import { apiFetch } from '@/utils/api';
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; 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>({ const { data, isLoading } = useQuery<LineProductsResponse>({
queryKey: ['lineProducts', brand, line], queryKey: ['lineProducts', brand, line],
queryFn: async () => { queryFn: async () => {
const res = await fetch( const res = await apiFetch(
`${config.apiUrl}/lines-aggregate/${encodeURIComponent(brand)}/${encodeURIComponent(line)}/products`, `${config.apiUrl}/lines-aggregate/${encodeURIComponent(brand)}/${encodeURIComponent(line)}/products`,
{ credentials: 'include' } { credentials: 'include' }
); );
@@ -484,7 +485,7 @@ export function ProductLines() {
const { data: listData, isLoading: isLoadingList, error: listError } = useQuery<LineResponse, Error>({ const { data: listData, isLoading: isLoadingList, error: listError } = useQuery<LineResponse, Error>({
queryKey: ['productLines', queryParams.toString()], queryKey: ['productLines', queryParams.toString()],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`${config.apiUrl}/lines-aggregate?${queryParams.toString()}`, { const res = await apiFetch(`${config.apiUrl}/lines-aggregate?${queryParams.toString()}`, {
credentials: 'include' credentials: 'include'
}); });
if (!res.ok) throw new Error(`Network response was not ok (${res.status})`); 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>({ const { data: statsData, isLoading: isLoadingStats } = useQuery<LineStats, Error>({
queryKey: ['productLineStats'], queryKey: ['productLineStats'],
queryFn: async () => { 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"); if (!res.ok) throw new Error("Failed to fetch stats");
return res.json(); return res.json();
}, },
@@ -505,7 +506,7 @@ export function ProductLines() {
const { data: filterOptions } = useQuery<LineFilterOptions, Error>({ const { data: filterOptions } = useQuery<LineFilterOptions, Error>({
queryKey: ['productLineFilterOptions'], queryKey: ['productLineFilterOptions'],
queryFn: async () => { 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"); if (!res.ok) throw new Error("Failed to fetch filter options");
return res.json(); return res.json();
}, },
+4 -3
View File
@@ -1,4 +1,5 @@
import React, { useState, useEffect, useMemo, useCallback } from 'react'; import React, { useState, useEffect, useMemo, useCallback } from 'react';
import { apiFetch } from '@/utils/api';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
import { useQuery, keepPreviousData } from '@tanstack/react-query'; import { useQuery, keepPreviousData } from '@tanstack/react-query';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
@@ -289,7 +290,7 @@ export function Products() {
params.append('showInvisible', 'true'); 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'); if (!response.ok) throw new Error('Failed to fetch products');
const data = await response.json(); const data = await response.json();
@@ -323,7 +324,7 @@ export function Products() {
queryKey: ['filterOptions'], queryKey: ['filterOptions'],
queryFn: async () => { queryFn: async () => {
try { 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'); if (!response.ok) throw new Error('Failed to fetch filter options');
const data = await response.json(); const data = await response.json();
@@ -348,7 +349,7 @@ export function Products() {
const params = new URLSearchParams(); const params = new URLSearchParams();
if (showNonReplenishable) params.append('showNonReplenishable', 'true'); if (showNonReplenishable) params.append('showNonReplenishable', 'true');
if (showInvisible) params.append('showInvisible', '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'); if (!response.ok) throw new Error('Failed to fetch summary');
return response.json(); return response.json();
}, },
+7 -6
View File
@@ -1,4 +1,5 @@
import { useEffect, useState, useRef, useMemo } from "react"; import { useEffect, useState, useRef, useMemo } from "react";
import { apiFetch } from '@/utils/api';
import OrderMetricsCard from "../components/purchase-orders/OrderMetricsCard"; import OrderMetricsCard from "../components/purchase-orders/OrderMetricsCard";
import VendorMetricsCard from "../components/purchase-orders/VendorMetricsCard"; import VendorMetricsCard from "../components/purchase-orders/VendorMetricsCard";
import CategoryMetricsCard from "../components/purchase-orders/CategoryMetricsCard"; import CategoryMetricsCard from "../components/purchase-orders/CategoryMetricsCard";
@@ -151,7 +152,7 @@ export default function PurchaseOrders() {
console.log("Fetching data with params:", searchParams.toString()); console.log("Fetching data with params:", searchParams.toString());
// Fetch orders first separately to handle errors better // 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) { if (!purchaseOrdersRes.ok) {
const errorText = await purchaseOrdersRes.text(); const errorText = await purchaseOrdersRes.text();
@@ -179,9 +180,9 @@ export default function PurchaseOrders() {
// Now fetch the additional data in parallel // Now fetch the additional data in parallel
const [vendorMetricsRes, costAnalysisRes, deliveryMetricsRes] = await Promise.all([ const [vendorMetricsRes, costAnalysisRes, deliveryMetricsRes] = await Promise.all([
fetch("/api/purchase-orders/vendor-metrics"), apiFetch("/api/purchase-orders/vendor-metrics"),
fetch("/api/purchase-orders/cost-analysis"), apiFetch("/api/purchase-orders/cost-analysis"),
fetch("/api/purchase-orders/delivery-metrics"), apiFetch("/api/purchase-orders/delivery-metrics"),
]); ]);
let vendorMetricsData = []; let vendorMetricsData = [];
@@ -369,8 +370,8 @@ export default function PurchaseOrders() {
const dateParam = oneYearAgo.toISOString().split("T")[0]; // Format as YYYY-MM-DD const dateParam = oneYearAgo.toISOString().split("T")[0]; // Format as YYYY-MM-DD
const [vendorResponse, categoryResponse] = await Promise.all([ const [vendorResponse, categoryResponse] = await Promise.all([
fetch(`/api/purchase-orders/vendor-analysis?since=${dateParam}`), apiFetch(`/api/purchase-orders/vendor-analysis?since=${dateParam}`),
fetch(`/api/purchase-orders/category-analysis?since=${dateParam}`), apiFetch(`/api/purchase-orders/category-analysis?since=${dateParam}`),
]); ]);
if (vendorResponse.ok) { if (vendorResponse.ok) {
+4 -3
View File
@@ -1,4 +1,5 @@
import { Fragment, useMemo, useState } from "react"; import { Fragment, useMemo, useState } from "react";
import { apiFetch } from '@/utils/api';
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { import {
Loader2, Loader2,
@@ -205,7 +206,7 @@ function ProductHistory({
supplierId: String(supplierId), supplierId: String(supplierId),
windowDays: String(windowDays), windowDays: String(windowDays),
}); });
const res = await fetch( const res = await apiFetch(
`/api/repeat-orders/${pid}/history?${params.toString()}` `/api/repeat-orders/${pid}/history?${params.toString()}`
); );
if (!res.ok) throw new Error("Failed to load history"); if (!res.ok) throw new Error("Failed to load history");
@@ -370,7 +371,7 @@ export default function RepeatOrders() {
const { data: suppliersData } = useQuery<{ suppliers: Supplier[] }>({ const { data: suppliersData } = useQuery<{ suppliers: Supplier[] }>({
queryKey: ["repeat-orders-suppliers"], queryKey: ["repeat-orders-suppliers"],
queryFn: async () => { 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"); if (!res.ok) throw new Error("Failed to load suppliers");
return res.json(); return res.json();
}, },
@@ -386,7 +387,7 @@ export default function RepeatOrders() {
minPoCount: String(minPoCount), minPoCount: String(minPoCount),
maxAvgQty: String(maxAvgQty), maxAvgQty: String(maxAvgQty),
}); });
const res = await fetch(`/api/repeat-orders?${params.toString()}`); const res = await apiFetch(`/api/repeat-orders?${params.toString()}`);
if (!res.ok) { if (!res.ok) {
const payload = await res.json().catch(() => ({})); const payload = await res.json().catch(() => ({}));
throw new Error(payload.error || "Failed to load repeat orders"); 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 { useEffect, useMemo, useRef, useState, type FormEvent, type MouseEvent } from "react";
import { apiFetch } from '@/utils/api';
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { Search, Loader2, PackageOpen, Copy, Check, ChevronsUpDown, X } from "lucide-react"; 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[] }>({ const { data: brandsData } = useQuery<{ brands: string[] }>({
queryKey: ["spec-lookup-brands"], queryKey: ["spec-lookup-brands"],
queryFn: async () => { 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"); if (!response.ok) throw new Error("Failed to load brands");
return response.json(); return response.json();
}, },
@@ -143,7 +144,7 @@ export default function SpecLookup() {
if (submitted?.company) params.set("company", submitted.company); if (submitted?.company) params.set("company", submitted.company);
if (submitted?.term) params.set("term", submitted.term); 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(() => ({})); const payload = await response.json().catch(() => ({}));
if (!response.ok) { if (!response.ok) {
+2 -1
View File
@@ -1,3 +1,4 @@
import { apiFetch } from '@/utils/api';
/** /**
* Import Audit Log API Service * Import Audit Log API Service
* *
@@ -31,7 +32,7 @@ export interface ImportAuditLogEntry {
*/ */
export async function createImportAuditLog(entry: ImportAuditLogEntry): Promise<void> { export async function createImportAuditLog(entry: ImportAuditLogEntry): Promise<void> {
try { try {
await fetch(BASE_URL, { await apiFetch(BASE_URL, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(entry), body: JSON.stringify(entry),
+8 -7
View File
@@ -4,6 +4,7 @@
* Handles all API calls for import session persistence. * Handles all API calls for import session persistence.
*/ */
import { apiFetch } from '@/utils/api';
import type { import type {
ImportSession, ImportSession,
ImportSessionListItem, ImportSessionListItem,
@@ -36,7 +37,7 @@ async function handleResponse<T>(response: Response): Promise<T> {
* List all sessions for a user (named + unnamed) * List all sessions for a user (named + unnamed)
*/ */
export async function listSessions(userId: number): Promise<ImportSessionListItem[]> { 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); 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) * Get a specific session by ID (includes full data)
*/ */
export async function getSession(id: number): Promise<ImportSession> { 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); return handleResponse<ImportSession>(response);
} }
@@ -52,7 +53,7 @@ export async function getSession(id: number): Promise<ImportSession> {
* Create a new named session * Create a new named session
*/ */
export async function createSession(data: ImportSessionCreateRequest): Promise<ImportSession> { export async function createSession(data: ImportSessionCreateRequest): Promise<ImportSession> {
const response = await fetch(BASE_URL, { const response = await apiFetch(BASE_URL, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data), body: JSON.stringify(data),
@@ -64,7 +65,7 @@ export async function createSession(data: ImportSessionCreateRequest): Promise<I
* Update an existing session by ID * Update an existing session by ID
*/ */
export async function updateSession(id: number, data: ImportSessionUpdateRequest): Promise<ImportSession> { 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', method: 'PUT',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data), body: JSON.stringify(data),
@@ -76,7 +77,7 @@ export async function updateSession(id: number, data: ImportSessionUpdateRequest
* Autosave - upsert the unnamed session for a user * Autosave - upsert the unnamed session for a user
*/ */
export async function autosaveSession(data: ImportSessionAutosaveRequest): Promise<ImportSession> { export async function autosaveSession(data: ImportSessionAutosaveRequest): Promise<ImportSession> {
const response = await fetch(`${BASE_URL}/autosave`, { const response = await apiFetch(`${BASE_URL}/autosave`, {
method: 'PUT', method: 'PUT',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data), body: JSON.stringify(data),
@@ -88,7 +89,7 @@ export async function autosaveSession(data: ImportSessionAutosaveRequest): Promi
* Delete a session by ID * Delete a session by ID
*/ */
export async function deleteSession(id: number): Promise<void> { export async function deleteSession(id: number): Promise<void> {
const response = await fetch(`${BASE_URL}/${id}`, { const response = await apiFetch(`${BASE_URL}/${id}`, {
method: 'DELETE', method: 'DELETE',
}); });
if (!response.ok) { if (!response.ok) {
@@ -108,7 +109,7 @@ export async function deleteSession(id: number): Promise<void> {
* Delete the unnamed/autosave session for a user * Delete the unnamed/autosave session for a user
*/ */
export async function deleteAutosaveSession(userId: number): Promise<void> { 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', method: 'DELETE',
}); });
// 404 is ok - means no autosave existed // 404 is ok - means no autosave existed
+4 -3
View File
@@ -1,3 +1,4 @@
import { apiFetch } from '@/utils/api';
export interface ImageChanges { export interface ImageChanges {
order: (number | string)[]; order: (number | string)[];
hidden: number[]; hidden: number[];
@@ -64,7 +65,7 @@ export async function submitProductEdit({
let response: Response; let response: Response;
try { try {
response = await fetch(targetUrl, fetchOptions); response = await apiFetch(targetUrl, fetchOptions);
} catch (networkError) { } catch (networkError) {
throw new Error( throw new Error(
networkError instanceof Error ? networkError.message : "Network request failed" networkError instanceof Error ? networkError.message : "Network request failed"
@@ -134,7 +135,7 @@ export async function submitTaxonomySet({
let response: Response; let response: Response;
try { try {
response = await fetch(targetUrl, fetchOptions); response = await apiFetch(targetUrl, fetchOptions);
} catch (networkError) { } catch (networkError) {
throw new Error( throw new Error(
networkError instanceof Error ? networkError.message : "Network request failed" networkError instanceof Error ? networkError.message : "Network request failed"
@@ -199,7 +200,7 @@ export async function submitImageChanges({
let response: Response; let response: Response;
try { try {
response = await fetch(targetUrl, fetchOptions); response = await apiFetch(targetUrl, fetchOptions);
} catch (networkError) { } catch (networkError) {
throw new Error( throw new Error(
networkError instanceof Error ? networkError.message : "Network request failed" networkError instanceof Error ? networkError.message : "Network request failed"
@@ -1,3 +1,4 @@
import { apiFetch } from '@/utils/api';
/** /**
* Product Editor Audit Log API Service * Product Editor Audit Log API Service
* *
@@ -27,7 +28,7 @@ export interface ProductEditorAuditLogEntry {
*/ */
export async function createProductEditorAuditLog(entry: ProductEditorAuditLogEntry): Promise<void> { export async function createProductEditorAuditLog(entry: ProductEditorAuditLogEntry): Promise<void> {
try { try {
await fetch(BASE_URL, { await apiFetch(BASE_URL, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(entry), 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