diff --git a/inventory/src/components/auth/FirstAccessiblePage.tsx b/inventory/src/components/auth/FirstAccessiblePage.tsx index 2a604ad..d517b2a 100644 --- a/inventory/src/components/auth/FirstAccessiblePage.tsx +++ b/inventory/src/components/auth/FirstAccessiblePage.tsx @@ -27,14 +27,12 @@ export function FirstAccessiblePage() { return null; } - // Admin users have access to all pages, so this component - // shouldn't be rendering for them (handled by App.tsx) - if (user.is_admin) { - return null; - } - - // Find the first page the user has access to + // Admins can access every page, otherwise check explicit permissions const firstAccessiblePage = PAGES.find(page => { + if (user.is_admin) { + return true; + } + return user.permissions?.includes(page.permission); }); diff --git a/inventory/src/components/product-import/steps/ImageUploadStep/ImageUploadStep.tsx b/inventory/src/components/product-import/steps/ImageUploadStep/ImageUploadStep.tsx index d8c650b..fde05b3 100644 --- a/inventory/src/components/product-import/steps/ImageUploadStep/ImageUploadStep.tsx +++ b/inventory/src/components/product-import/steps/ImageUploadStep/ImageUploadStep.tsx @@ -1,4 +1,4 @@ -import { useCallback, useState, useRef, useEffect, createRef } from "react"; +import { useCallback, useState, useRef, useEffect, createRef, useContext } from "react"; import { useRsi } from "../../hooks/useRsi"; import { Button } from "@/components/ui/button"; import { Loader2 } from "lucide-react"; @@ -22,12 +22,16 @@ import { useProductImagesInit } from "./hooks/useProductImagesInit"; import { useProductImageOperations } from "./hooks/useProductImageOperations"; import { useBulkImageUpload } from "./hooks/useBulkImageUpload"; import { useUrlImageUpload } from "./hooks/useUrlImageUpload"; +import { Switch } from "@/components/ui/switch"; +import { Label } from "@/components/ui/label"; +import { AuthContext } from "@/contexts/AuthContext"; +import type { SubmitOptions } from "../../types"; interface Props { data: Product[]; file: File; onBack?: () => void; - onSubmit: (data: Product[], file: File) => void | Promise; + onSubmit: (data: Product[], file: File, options: SubmitOptions) => void | Promise; } export const ImageUploadStep = ({ @@ -39,6 +43,17 @@ export const ImageUploadStep = ({ useRsi(); const [isSubmitting, setIsSubmitting] = useState(false); const fileInputRefs = useRef<{ [key: number]: React.RefObject }>({}); + const { user } = useContext(AuthContext); + const hasDebugPermission = Boolean(user?.is_admin || user?.permissions?.includes("admin:debug")); + const [targetEnvironment, setTargetEnvironment] = useState( + hasDebugPermission ? "dev" : "prod" + ); + const [useTestDataSource, setUseTestDataSource] = useState(hasDebugPermission); + + useEffect(() => { + setTargetEnvironment(hasDebugPermission ? "dev" : "prod"); + setUseTestDataSource(hasDebugPermission); + }, [hasDebugPermission]); // Use our hook for product images initialization const { productImages, setProductImages, getFullImageUrl } = useProductImagesInit(data); @@ -166,7 +181,12 @@ export const ImageUploadStep = ({ }; }); - await onSubmit(updatedData, file); + const submitOptions: SubmitOptions = { + targetEnvironment, + useTestDataSource, + }; + + await onSubmit(updatedData, file, submitOptions); } catch (error) { console.error('Submit error:', error); toast.error(`Failed to submit: ${error instanceof Error ? error.message : 'Unknown error'}`); @@ -275,21 +295,62 @@ export const ImageUploadStep = ({ {/* Footer - fixed at bottom */} -
+
{onBack && ( )} - +
+
+
+ setTargetEnvironment(checked ? "dev" : "prod")} + /> +
+ +

+ {targetEnvironment === "dev" + ? "work-test-backend.acherryontop.com" + : "backend.acherryontop.com"} +

+
+
+
+ setUseTestDataSource(checked)} + /> +
+ +

Submit to the testing database

+
+
+ {!hasDebugPermission && ( +

+ Requires admin:debug permission to change +

+ )} +
+ +
); -}; \ No newline at end of file +}; diff --git a/inventory/src/components/product-import/steps/UploadFlow.tsx b/inventory/src/components/product-import/steps/UploadFlow.tsx index 7419859..0a7f9bf 100644 --- a/inventory/src/components/product-import/steps/UploadFlow.tsx +++ b/inventory/src/components/product-import/steps/UploadFlow.tsx @@ -243,14 +243,14 @@ export const UploadFlow = ({ state, onNext, onBack }: Props) => { data={state.data} file={state.file} onBack={onBack} - onSubmit={(data, file) => { + onSubmit={(data, file, options) => { // Create a Result object from the array data const result = { validData: data as Data[], invalidData: [] as Data[], all: data as Data[] }; - onSubmit(result, file); + onSubmit(result, file, options); }} /> ) diff --git a/inventory/src/components/product-import/types.ts b/inventory/src/components/product-import/types.ts index 455331e..807fa71 100644 --- a/inventory/src/components/product-import/types.ts +++ b/inventory/src/components/product-import/types.ts @@ -4,6 +4,11 @@ import type { TranslationsRSIProps } from "./translationsRSIProps" import type { Columns } from "./steps/MatchColumnsStep/MatchColumnsStep" import type { StepState } from "./steps/UploadFlow" +export type SubmitOptions = { + targetEnvironment: "dev" | "prod" + useTestDataSource: boolean +} + export type RsiProps = { // Is modal visible. isOpen: boolean @@ -24,7 +29,7 @@ export type RsiProps = { // Runs after column matching and on entry change tableHook?: TableHook // Function called after user finishes the flow. You can return a promise that will be awaited. - onSubmit: (data: Result, file: File) => void | Promise + onSubmit: (data: Result, file: File, options: SubmitOptions) => void | Promise // Allows submitting with errors. Default: true allowInvalidSubmit?: boolean // Enable navigation in stepper component and show back button. Default: false diff --git a/inventory/src/pages/Import.tsx b/inventory/src/pages/Import.tsx index b06c0f9..c45c96a 100644 --- a/inventory/src/pages/Import.tsx +++ b/inventory/src/pages/Import.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, useContext } from "react"; import { ReactSpreadsheetImport, StepType } from "@/components/product-import"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; @@ -8,8 +8,10 @@ import { motion } from "framer-motion"; import { useQuery } from "@tanstack/react-query"; import config from "@/config"; import { Loader2 } from "lucide-react"; -import type { DataValue, FieldType, Result } from "@/components/product-import/types"; +import type { DataValue, FieldType, Result, SubmitOptions } from "@/components/product-import/types"; import { BASE_IMPORT_FIELDS, type ImportFieldKey } from "@/components/product-import/config"; +import { submitNewProducts } from "@/services/apiv2"; +import { AuthContext } from "@/contexts/AuthContext"; export function Import() { const [isOpen, setIsOpen] = useState(false); @@ -20,6 +22,7 @@ export function Import() { const [selectedCompany, setSelectedCompany] = useState(null); const [selectedLine, setSelectedLine] = useState(null); const [startFromScratch, setStartFromScratch] = useState(false); + const { user } = useContext(AuthContext); // Fetch initial field options from the API const { data: fieldOptions, isLoading: isLoadingOptions } = useQuery({ @@ -266,7 +269,7 @@ export function Import() { return stringValue; }; - const handleData = async (data: ImportResult, _file: File) => { + const handleData = async (data: ImportResult, _file: File, submitOptions: SubmitOptions) => { try { const rows = (data.all?.length ? data.all : data.validData) ?? []; const formattedRows: NormalizedProduct[] = rows.map((row) => { @@ -292,12 +295,28 @@ export function Import() { } as NormalizedProduct; }); + const response = await submitNewProducts({ + products: formattedRows, + environment: submitOptions?.targetEnvironment ?? "prod", + useTestDataSource: Boolean(submitOptions?.useTestDataSource), + employeeId: user?.id ?? undefined, + }); + + if (!response.success) { + throw new Error(response.message || "Failed to submit products"); + } + setImportedData(formattedRows); setIsOpen(false); - toast.success("Data imported successfully"); + + const successMessage = response.message + ? response.message + : `Submitted ${formattedRows.length} product${formattedRows.length === 1 ? "" : "s"} successfully`; + + toast.success(successMessage); } catch (error) { - toast.error("Failed to import data"); console.error("Import error:", error); + throw error instanceof Error ? error : new Error("Failed to import data"); } }; diff --git a/inventory/src/services/apiv2.ts b/inventory/src/services/apiv2.ts new file mode 100644 index 0000000..8c77453 --- /dev/null +++ b/inventory/src/services/apiv2.ts @@ -0,0 +1,84 @@ +export interface SubmitNewProductsArgs { + products: Array>; + environment: "dev" | "prod"; + useTestDataSource: boolean; + employeeId?: number; +} + +export interface SubmitNewProductsResponse { + success: boolean; + message?: string; + data?: unknown; + error?: unknown; +} + +const DEV_ENDPOINT = "https://work-test-backend.acherryontop.com/apiv2/product/setup_new"; +const PROD_ENDPOINT = "https://backend.acherryontop.com/apiv2/product/setup_new"; + +const isHtmlResponse = (payload: string) => { + const trimmed = payload.trim(); + return trimmed.startsWith(" { + const authToken = import.meta.env.VITE_APIV2_AUTH_TOKEN; + + if (!authToken) { + throw new Error("VITE_APIV2_AUTH_TOKEN is not configured"); + } + + const baseUrl = environment === "dev" ? DEV_ENDPOINT : PROD_ENDPOINT; + const targetUrl = useTestDataSource ? `${baseUrl}?use_test_data_source=1` : baseUrl; + + const payload = new URLSearchParams(); + payload.append("auth", authToken); + + const serializedProducts = JSON.stringify(products); + payload.append("products", serializedProducts); + + let response: Response; + + try { + response = await fetch(targetUrl, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded; charset=utf-8", + }, + body: payload, + }); + } catch (networkError) { + throw new Error(networkError instanceof Error ? networkError.message : "Network request failed"); + } + + const rawBody = await response.text(); + + if (isHtmlResponse(rawBody)) { + throw new Error("Backend authentication required. Please ensure you are logged into the backend system."); + } + + let parsed: SubmitNewProductsResponse | null = null; + try { + parsed = JSON.parse(rawBody); + } catch { + const message = `Unexpected response from backend (${response.status}).`; + throw new Error(message); + } + + if (!response.ok) { + throw new Error(parsed?.message || `Request failed with status ${response.status}`); + } + + if (!parsed) { + throw new Error("Empty response from backend"); + } + + if (!parsed.success) { + throw new Error(parsed.message || "Backend rejected product submission"); + } + + return parsed; +}