Add initial backend api connection, fix issue with admin first load page

This commit is contained in:
2025-10-03 22:46:06 -04:00
parent 451d5f0b3b
commit 920c33d119
6 changed files with 195 additions and 28 deletions

View File

@@ -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);
});

View File

@@ -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,20 +295,61 @@ 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>
);

View File

@@ -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);
}}
/>
)

View File

@@ -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

View File

@@ -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");
}
};

View 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;
}