Add initial backend api connection, fix issue with admin first load page
This commit is contained in:
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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<any>;
|
||||
onSubmit: (data: Product[], file: File, options: SubmitOptions) => void | Promise<any>;
|
||||
}
|
||||
|
||||
export const ImageUploadStep = ({
|
||||
@@ -39,6 +43,17 @@ export const ImageUploadStep = ({
|
||||
useRsi();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const fileInputRefs = useRef<{ [key: number]: React.RefObject<HTMLInputElement> }>({});
|
||||
const { user } = useContext(AuthContext);
|
||||
const hasDebugPermission = Boolean(user?.is_admin || user?.permissions?.includes("admin:debug"));
|
||||
const [targetEnvironment, setTargetEnvironment] = useState<SubmitOptions["targetEnvironment"]>(
|
||||
hasDebugPermission ? "dev" : "prod"
|
||||
);
|
||||
const [useTestDataSource, setUseTestDataSource] = useState<boolean>(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 = ({
|
||||
</div>
|
||||
|
||||
{/* Footer - fixed at bottom */}
|
||||
<div className="flex items-center justify-between border-t bg-muted px-8 py-4 mt-1 shrink-0">
|
||||
<div className="flex flex-wrap items-center justify-between gap-4 border-t bg-muted px-8 py-4 mt-1 shrink-0">
|
||||
{onBack && (
|
||||
<Button variant="outline" onClick={onBack}>
|
||||
Back
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
className="ml-auto"
|
||||
onClick={handleSubmit}
|
||||
disabled={isSubmitting || unassignedImages.length > 0}
|
||||
>
|
||||
{isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
{unassignedImages.length > 0 ? 'Assign all images first' : 'Submit'}
|
||||
</Button>
|
||||
<div className="flex flex-1 flex-wrap items-center justify-end gap-6">
|
||||
<div className="flex flex-col gap-3 text-sm">
|
||||
<div className="flex items-center gap-3">
|
||||
<Switch
|
||||
id="product-import-api-environment"
|
||||
checked={targetEnvironment === "dev"}
|
||||
disabled={!hasDebugPermission}
|
||||
onCheckedChange={(checked) => setTargetEnvironment(checked ? "dev" : "prod")}
|
||||
/>
|
||||
<div>
|
||||
<Label htmlFor="product-import-api-environment" className="text-sm font-medium">
|
||||
Use development backend
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{targetEnvironment === "dev"
|
||||
? "work-test-backend.acherryontop.com"
|
||||
: "backend.acherryontop.com"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Switch
|
||||
id="product-import-api-test-data"
|
||||
checked={useTestDataSource}
|
||||
disabled={!hasDebugPermission}
|
||||
onCheckedChange={(checked) => setUseTestDataSource(checked)}
|
||||
/>
|
||||
<div>
|
||||
<Label htmlFor="product-import-api-test-data" className="text-sm font-medium">
|
||||
Use test data source
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">Submit to the testing database</p>
|
||||
</div>
|
||||
</div>
|
||||
{!hasDebugPermission && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Requires admin:debug permission to change
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
className="min-w-[140px]"
|
||||
onClick={handleSubmit}
|
||||
disabled={isSubmitting || unassignedImages.length > 0}
|
||||
>
|
||||
{isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
{unassignedImages.length > 0 ? 'Assign all images first' : 'Submit'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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<string>[],
|
||||
invalidData: [] as Data<string>[],
|
||||
all: data as Data<string>[]
|
||||
};
|
||||
onSubmit(result, file);
|
||||
onSubmit(result, file, options);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -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<T extends string> = {
|
||||
// Is modal visible.
|
||||
isOpen: boolean
|
||||
@@ -24,7 +29,7 @@ export type RsiProps<T extends string> = {
|
||||
// Runs after column matching and on entry change
|
||||
tableHook?: TableHook<T>
|
||||
// Function called after user finishes the flow. You can return a promise that will be awaited.
|
||||
onSubmit: (data: Result<T>, file: File) => void | Promise<any>
|
||||
onSubmit: (data: Result<T>, file: File, options: SubmitOptions) => void | Promise<any>
|
||||
// Allows submitting with errors. Default: true
|
||||
allowInvalidSubmit?: boolean
|
||||
// Enable navigation in stepper component and show back button. Default: false
|
||||
|
||||
@@ -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<string | null>(null);
|
||||
const [selectedLine, setSelectedLine] = useState<string | null>(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");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
84
inventory/src/services/apiv2.ts
Normal file
84
inventory/src/services/apiv2.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
export interface SubmitNewProductsArgs {
|
||||
products: Array<Record<string, unknown>>;
|
||||
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("<!DOCTYPE html") || trimmed.startsWith("<html");
|
||||
};
|
||||
|
||||
export async function submitNewProducts({
|
||||
products,
|
||||
environment,
|
||||
useTestDataSource,
|
||||
}: SubmitNewProductsArgs): Promise<SubmitNewProductsResponse> {
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user