Compare commits
2 Commits
7eae4a0b29
...
1fcbf54989
| Author | SHA1 | Date | |
|---|---|---|---|
| 1fcbf54989 | |||
| ce75496770 |
@@ -2,10 +2,14 @@ CREATE TABLE users (
|
|||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
username VARCHAR(255) NOT NULL UNIQUE,
|
username VARCHAR(255) NOT NULL UNIQUE,
|
||||||
password VARCHAR(255) NOT NULL,
|
password VARCHAR(255) NOT NULL,
|
||||||
|
email VARCHAR UNIQUE,
|
||||||
|
is_admin BOOLEAN DEFAULT FALSE,
|
||||||
|
is_active BOOLEAN DEFAULT TRUE,
|
||||||
|
last_login TIMESTAMP WITH TIME ZONE,
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
-- Function to update the updated_at timestamp
|
-- Function to update the updated_at timestamp
|
||||||
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||||
RETURNS TRIGGER AS $$
|
RETURNS TRIGGER AS $$
|
||||||
@@ -18,14 +22,6 @@ $$ language 'plpgsql';
|
|||||||
-- Sequence and defined type for users table if not exists
|
-- Sequence and defined type for users table if not exists
|
||||||
CREATE SEQUENCE IF NOT EXISTS users_id_seq;
|
CREATE SEQUENCE IF NOT EXISTS users_id_seq;
|
||||||
|
|
||||||
-- Update users table with new fields
|
|
||||||
ALTER TABLE "public"."users"
|
|
||||||
ADD COLUMN IF NOT EXISTS "email" varchar UNIQUE,
|
|
||||||
ADD COLUMN IF NOT EXISTS "is_admin" boolean DEFAULT FALSE,
|
|
||||||
ADD COLUMN IF NOT EXISTS "is_active" boolean DEFAULT TRUE,
|
|
||||||
ADD COLUMN IF NOT EXISTS "last_login" timestamp with time zone,
|
|
||||||
ADD COLUMN IF NOT EXISTS "updated_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP;
|
|
||||||
|
|
||||||
-- Create permissions table
|
-- Create permissions table
|
||||||
CREATE TABLE IF NOT EXISTS "public"."permissions" (
|
CREATE TABLE IF NOT EXISTS "public"."permissions" (
|
||||||
"id" SERIAL PRIMARY KEY,
|
"id" SERIAL PRIMARY KEY,
|
||||||
@@ -58,8 +54,7 @@ CREATE TRIGGER update_permissions_updated_at
|
|||||||
FOR EACH ROW
|
FOR EACH ROW
|
||||||
EXECUTE FUNCTION update_updated_at_column();
|
EXECUTE FUNCTION update_updated_at_column();
|
||||||
|
|
||||||
-- Insert default permissions by page
|
-- Insert default permissions by page - only the ones used in application
|
||||||
-- Core page access permissions
|
|
||||||
INSERT INTO permissions (name, code, description, category) VALUES
|
INSERT INTO permissions (name, code, description, category) VALUES
|
||||||
('Dashboard Access', 'access:dashboard', 'Can access the Dashboard page', 'Pages'),
|
('Dashboard Access', 'access:dashboard', 'Can access the Dashboard page', 'Pages'),
|
||||||
('Products Access', 'access:products', 'Can access the Products page', 'Pages'),
|
('Products Access', 'access:products', 'Can access the Products page', 'Pages'),
|
||||||
@@ -73,52 +68,14 @@ INSERT INTO permissions (name, code, description, category) VALUES
|
|||||||
('AI Validation Debug Access', 'access:ai_validation_debug', 'Can access the AI Validation Debug page', 'Pages')
|
('AI Validation Debug Access', 'access:ai_validation_debug', 'Can access the AI Validation Debug page', 'Pages')
|
||||||
ON CONFLICT (code) DO NOTHING;
|
ON CONFLICT (code) DO NOTHING;
|
||||||
|
|
||||||
-- Granular permissions for Products
|
-- Settings section permissions
|
||||||
INSERT INTO permissions (name, code, description, category) VALUES
|
|
||||||
('View Products', 'view:products', 'Can view product listings', 'Products'),
|
|
||||||
('Create Products', 'create:products', 'Can create new products', 'Products'),
|
|
||||||
('Edit Products', 'edit:products', 'Can edit product details', 'Products'),
|
|
||||||
('Delete Products', 'delete:products', 'Can delete products', 'Products')
|
|
||||||
ON CONFLICT (code) DO NOTHING;
|
|
||||||
|
|
||||||
-- Granular permissions for Categories
|
|
||||||
INSERT INTO permissions (name, code, description, category) VALUES
|
|
||||||
('View Categories', 'view:categories', 'Can view categories', 'Categories'),
|
|
||||||
('Create Categories', 'create:categories', 'Can create new categories', 'Categories'),
|
|
||||||
('Edit Categories', 'edit:categories', 'Can edit categories', 'Categories'),
|
|
||||||
('Delete Categories', 'delete:categories', 'Can delete categories', 'Categories')
|
|
||||||
ON CONFLICT (code) DO NOTHING;
|
|
||||||
|
|
||||||
-- Granular permissions for Vendors
|
|
||||||
INSERT INTO permissions (name, code, description, category) VALUES
|
|
||||||
('View Vendors', 'view:vendors', 'Can view vendors', 'Vendors'),
|
|
||||||
('Create Vendors', 'create:vendors', 'Can create new vendors', 'Vendors'),
|
|
||||||
('Edit Vendors', 'edit:vendors', 'Can edit vendors', 'Vendors'),
|
|
||||||
('Delete Vendors', 'delete:vendors', 'Can delete vendors', 'Vendors')
|
|
||||||
ON CONFLICT (code) DO NOTHING;
|
|
||||||
|
|
||||||
-- Granular permissions for Purchase Orders
|
|
||||||
INSERT INTO permissions (name, code, description, category) VALUES
|
|
||||||
('View Purchase Orders', 'view:purchase_orders', 'Can view purchase orders', 'Purchase Orders'),
|
|
||||||
('Create Purchase Orders', 'create:purchase_orders', 'Can create new purchase orders', 'Purchase Orders'),
|
|
||||||
('Edit Purchase Orders', 'edit:purchase_orders', 'Can edit purchase orders', 'Purchase Orders'),
|
|
||||||
('Delete Purchase Orders', 'delete:purchase_orders', 'Can delete purchase orders', 'Purchase Orders')
|
|
||||||
ON CONFLICT (code) DO NOTHING;
|
|
||||||
|
|
||||||
-- User management permissions
|
|
||||||
INSERT INTO permissions (name, code, description, category) VALUES
|
|
||||||
('View Users', 'view:users', 'Can view user accounts', 'Users'),
|
|
||||||
('Create Users', 'create:users', 'Can create user accounts', 'Users'),
|
|
||||||
('Edit Users', 'edit:users', 'Can modify user accounts', 'Users'),
|
|
||||||
('Delete Users', 'delete:users', 'Can delete user accounts', 'Users'),
|
|
||||||
('Manage Permissions', 'manage:permissions', 'Can assign permissions to users', 'Users')
|
|
||||||
ON CONFLICT (code) DO NOTHING;
|
|
||||||
|
|
||||||
-- System permissions
|
|
||||||
INSERT INTO permissions (name, code, description, category) VALUES
|
INSERT INTO permissions (name, code, description, category) VALUES
|
||||||
('Run Calculations', 'run:calculations', 'Can trigger system calculations', 'System'),
|
('Data Management', 'settings:data_management', 'Access to the Data Management settings section', 'Settings'),
|
||||||
('Import Data', 'import:data', 'Can import data into the system', 'System'),
|
('Stock Management', 'settings:stock_management', 'Access to the Stock Management settings section', 'Settings'),
|
||||||
('System Settings', 'edit:system_settings', 'Can modify system settings', 'System')
|
('Performance Metrics', 'settings:performance_metrics', 'Access to the Performance Metrics settings section', 'Settings'),
|
||||||
|
('Calculation Settings', 'settings:calculation_settings', 'Access to the Calculation Settings section', 'Settings'),
|
||||||
|
('Template Management', 'settings:templates', 'Access to the Template Management settings section', 'Settings'),
|
||||||
|
('User Management', 'settings:user_management', 'Access to the User Management settings section', 'Settings')
|
||||||
ON CONFLICT (code) DO NOTHING;
|
ON CONFLICT (code) DO NOTHING;
|
||||||
|
|
||||||
-- Set any existing users as admin
|
-- Set any existing users as admin
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ import Forecasting from "@/pages/Forecasting";
|
|||||||
import { Vendors } from '@/pages/Vendors';
|
import { Vendors } from '@/pages/Vendors';
|
||||||
import { Categories } from '@/pages/Categories';
|
import { Categories } from '@/pages/Categories';
|
||||||
import { Import } from '@/pages/Import';
|
import { Import } from '@/pages/Import';
|
||||||
import { AiValidationDebug } from "@/pages/AiValidationDebug"
|
|
||||||
import { AuthProvider } from './contexts/AuthContext';
|
import { AuthProvider } from './contexts/AuthContext';
|
||||||
import { Protected } from './components/auth/Protected';
|
import { Protected } from './components/auth/Protected';
|
||||||
|
import { FirstAccessiblePage } from './components/auth/FirstAccessiblePage';
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
@@ -78,6 +78,11 @@ function App() {
|
|||||||
<MainLayout />
|
<MainLayout />
|
||||||
</RequireAuth>
|
</RequireAuth>
|
||||||
}>
|
}>
|
||||||
|
<Route index element={
|
||||||
|
<Protected page="dashboard" fallback={<FirstAccessiblePage />}>
|
||||||
|
<Dashboard />
|
||||||
|
</Protected>
|
||||||
|
} />
|
||||||
<Route path="/" element={
|
<Route path="/" element={
|
||||||
<Protected page="dashboard">
|
<Protected page="dashboard">
|
||||||
<Dashboard />
|
<Dashboard />
|
||||||
@@ -123,11 +128,6 @@ function App() {
|
|||||||
<Forecasting />
|
<Forecasting />
|
||||||
</Protected>
|
</Protected>
|
||||||
} />
|
} />
|
||||||
<Route path="/ai-validation/debug" element={
|
|
||||||
<Protected page="ai_validation_debug">
|
|
||||||
<AiValidationDebug />
|
|
||||||
</Protected>
|
|
||||||
} />
|
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
44
inventory/src/components/auth/FirstAccessiblePage.tsx
Normal file
44
inventory/src/components/auth/FirstAccessiblePage.tsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { useContext } from "react";
|
||||||
|
import { Navigate } from "react-router-dom";
|
||||||
|
import { AuthContext } from "@/contexts/AuthContext";
|
||||||
|
|
||||||
|
// Define available pages in order of priority
|
||||||
|
const PAGES = [
|
||||||
|
{ path: "/products", permission: "access:products" },
|
||||||
|
{ path: "/categories", permission: "access:categories" },
|
||||||
|
{ path: "/vendors", permission: "access:vendors" },
|
||||||
|
{ path: "/purchase-orders", permission: "access:purchase_orders" },
|
||||||
|
{ path: "/analytics", permission: "access:analytics" },
|
||||||
|
{ path: "/forecasting", permission: "access:forecasting" },
|
||||||
|
{ path: "/import", permission: "access:import" },
|
||||||
|
{ path: "/settings", permission: "access:settings" },
|
||||||
|
{ path: "/ai-validation/debug", permission: "access:ai_validation_debug" }
|
||||||
|
];
|
||||||
|
|
||||||
|
export function FirstAccessiblePage() {
|
||||||
|
const { user } = useContext(AuthContext);
|
||||||
|
|
||||||
|
// If user isn't loaded yet, don't render anything
|
||||||
|
if (!user) {
|
||||||
|
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
|
||||||
|
const firstAccessiblePage = PAGES.find(page => {
|
||||||
|
return user.permissions?.includes(page.permission);
|
||||||
|
});
|
||||||
|
|
||||||
|
// If we found a page, redirect to it
|
||||||
|
if (firstAccessiblePage) {
|
||||||
|
return <Navigate to={firstAccessiblePage.path} replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If user has no access to any page, redirect to login
|
||||||
|
return <Navigate to="/login" replace />;
|
||||||
|
}
|
||||||
@@ -160,6 +160,7 @@ export function AppSidebar() {
|
|||||||
</SidebarGroupContent>
|
</SidebarGroupContent>
|
||||||
</SidebarGroup>
|
</SidebarGroup>
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
|
<SidebarSeparator />
|
||||||
<SidebarFooter>
|
<SidebarFooter>
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { AiValidationDialogs } from '../../../components/AiValidationDialogs';
|
||||||
|
import { Product } from '../../../types/product';
|
||||||
|
import { config } from '../../../config';
|
||||||
|
|
||||||
|
interface CurrentPrompt {
|
||||||
|
isOpen: boolean;
|
||||||
|
prompt: string;
|
||||||
|
isLoading: boolean;
|
||||||
|
debugData?: {
|
||||||
|
taxonomyStats: {
|
||||||
|
categories: number;
|
||||||
|
themes: number;
|
||||||
|
colors: number;
|
||||||
|
taxCodes: number;
|
||||||
|
sizeCategories: number;
|
||||||
|
suppliers: number;
|
||||||
|
companies: number;
|
||||||
|
artists: number;
|
||||||
|
} | null;
|
||||||
|
basePrompt: string;
|
||||||
|
sampleFullPrompt: string;
|
||||||
|
promptLength: number;
|
||||||
|
estimatedProcessingTime?: {
|
||||||
|
seconds: number | null;
|
||||||
|
sampleCount: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const ValidationStepNew: React.FC = () => {
|
||||||
|
const [aiValidationProgress, setAiValidationProgress] = useState(0);
|
||||||
|
const [aiValidationDetails, setAiValidationDetails] = useState('');
|
||||||
|
const [currentPrompt, setCurrentPrompt] = useState<CurrentPrompt>({
|
||||||
|
isOpen: false,
|
||||||
|
prompt: '',
|
||||||
|
isLoading: true,
|
||||||
|
});
|
||||||
|
const [isChangeReverted, setIsChangeReverted] = useState(false);
|
||||||
|
const [fieldData, setFieldData] = useState<Product[]>([]);
|
||||||
|
|
||||||
|
const showCurrentPrompt = async (products: Product[]) => {
|
||||||
|
setCurrentPrompt((prev) => ({ ...prev, isOpen: true, isLoading: true }));
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get the prompt
|
||||||
|
const promptResponse = await fetch(`${config.apiUrl}/ai-validation/prompt`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ products })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!promptResponse.ok) {
|
||||||
|
throw new Error('Failed to fetch AI prompt');
|
||||||
|
}
|
||||||
|
|
||||||
|
const promptData = await promptResponse.json();
|
||||||
|
|
||||||
|
// Get the debug data in the same request or as a separate request
|
||||||
|
const debugResponse = await fetch(`${config.apiUrl}/ai-validation/debug-info`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ prompt: promptData.prompt })
|
||||||
|
});
|
||||||
|
|
||||||
|
let debugData;
|
||||||
|
if (debugResponse.ok) {
|
||||||
|
debugData = await debugResponse.json();
|
||||||
|
} else {
|
||||||
|
// If debug-info fails, use a fallback to get taxonomy stats
|
||||||
|
const fallbackResponse = await fetch(`${config.apiUrl}/ai-validation/debug`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ products: [products[0]] }) // Use first product for stats
|
||||||
|
});
|
||||||
|
|
||||||
|
if (fallbackResponse.ok) {
|
||||||
|
debugData = await fallbackResponse.json();
|
||||||
|
// Set promptLength correctly from the actual prompt
|
||||||
|
debugData.promptLength = promptData.prompt.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentPrompt((prev) => ({
|
||||||
|
...prev,
|
||||||
|
prompt: promptData.prompt,
|
||||||
|
isLoading: false,
|
||||||
|
debugData: debugData
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching prompt:', error);
|
||||||
|
setCurrentPrompt((prev) => ({
|
||||||
|
...prev,
|
||||||
|
prompt: 'Error loading prompt',
|
||||||
|
isLoading: false
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const revertAiChange = () => {
|
||||||
|
setIsChangeReverted(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFieldDisplayValueWithHighlight = (value: string, highlight: string) => {
|
||||||
|
// Implementation of getFieldDisplayValueWithHighlight
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<AiValidationDialogs
|
||||||
|
aiValidationProgress={aiValidationProgress}
|
||||||
|
aiValidationDetails={aiValidationDetails}
|
||||||
|
currentPrompt={currentPrompt}
|
||||||
|
setAiValidationProgress={setAiValidationProgress}
|
||||||
|
setAiValidationDetails={setAiValidationDetails}
|
||||||
|
setCurrentPrompt={setCurrentPrompt}
|
||||||
|
revertAiChange={revertAiChange}
|
||||||
|
isChangeReverted={isChangeReverted}
|
||||||
|
getFieldDisplayValueWithHighlight={getFieldDisplayValueWithHighlight}
|
||||||
|
fields={fieldData}
|
||||||
|
debugData={currentPrompt.debugData}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ValidationStepNew;
|
||||||
@@ -1,23 +1,72 @@
|
|||||||
import React from 'react';
|
import React, { useState } from "react";
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
|
import {
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
Dialog,
|
||||||
import { Button } from '@/components/ui/button';
|
DialogContent,
|
||||||
import { Loader2, CheckIcon } from 'lucide-react';
|
DialogHeader,
|
||||||
import { Code } from '@/components/ui/code';
|
DialogTitle,
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
DialogDescription,
|
||||||
import { AiValidationDetails, AiValidationProgress, CurrentPrompt } from '../hooks/useAiValidation';
|
} from "@/components/ui/dialog";
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Loader2, CheckIcon } from "lucide-react";
|
||||||
|
import { Code } from "@/components/ui/code";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
import {
|
||||||
|
AiValidationDetails,
|
||||||
|
AiValidationProgress,
|
||||||
|
CurrentPrompt,
|
||||||
|
} from "../hooks/useAiValidation";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
|
||||||
|
interface TaxonomyStats {
|
||||||
|
categories: number;
|
||||||
|
themes: number;
|
||||||
|
colors: number;
|
||||||
|
taxCodes: number;
|
||||||
|
sizeCategories: number;
|
||||||
|
suppliers: number;
|
||||||
|
companies: number;
|
||||||
|
artists: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DebugData {
|
||||||
|
taxonomyStats: TaxonomyStats | null;
|
||||||
|
basePrompt: string;
|
||||||
|
sampleFullPrompt: string;
|
||||||
|
promptLength: number;
|
||||||
|
estimatedProcessingTime?: {
|
||||||
|
seconds: number | null;
|
||||||
|
sampleCount: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface AiValidationDialogsProps {
|
interface AiValidationDialogsProps {
|
||||||
aiValidationProgress: AiValidationProgress;
|
aiValidationProgress: AiValidationProgress;
|
||||||
aiValidationDetails: AiValidationDetails;
|
aiValidationDetails: AiValidationDetails;
|
||||||
currentPrompt: CurrentPrompt;
|
currentPrompt: CurrentPrompt;
|
||||||
setAiValidationProgress: React.Dispatch<React.SetStateAction<AiValidationProgress>>;
|
setAiValidationProgress: React.Dispatch<
|
||||||
setAiValidationDetails: React.Dispatch<React.SetStateAction<AiValidationDetails>>;
|
React.SetStateAction<AiValidationProgress>
|
||||||
|
>;
|
||||||
|
setAiValidationDetails: React.Dispatch<
|
||||||
|
React.SetStateAction<AiValidationDetails>
|
||||||
|
>;
|
||||||
setCurrentPrompt: React.Dispatch<React.SetStateAction<CurrentPrompt>>;
|
setCurrentPrompt: React.Dispatch<React.SetStateAction<CurrentPrompt>>;
|
||||||
revertAiChange: (productIndex: number, fieldKey: string) => void;
|
revertAiChange: (productIndex: number, fieldKey: string) => void;
|
||||||
isChangeReverted: (productIndex: number, fieldKey: string) => boolean;
|
isChangeReverted: (productIndex: number, fieldKey: string) => boolean;
|
||||||
getFieldDisplayValueWithHighlight: (fieldKey: string, originalValue: any, correctedValue: any) => { originalHtml: string, correctedHtml: string };
|
getFieldDisplayValueWithHighlight: (
|
||||||
|
fieldKey: string,
|
||||||
|
originalValue: any,
|
||||||
|
correctedValue: any
|
||||||
|
) => { originalHtml: string; correctedHtml: string };
|
||||||
fields: readonly any[];
|
fields: readonly any[];
|
||||||
|
debugData?: DebugData;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
|
export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
|
||||||
@@ -30,41 +79,192 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
|
|||||||
revertAiChange,
|
revertAiChange,
|
||||||
isChangeReverted,
|
isChangeReverted,
|
||||||
getFieldDisplayValueWithHighlight,
|
getFieldDisplayValueWithHighlight,
|
||||||
fields
|
fields,
|
||||||
|
debugData,
|
||||||
}) => {
|
}) => {
|
||||||
|
const [costPerMillionTokens, setCostPerMillionTokens] = useState(2.5); // Default cost
|
||||||
|
|
||||||
|
// Format time helper
|
||||||
|
const formatTime = (seconds: number): string => {
|
||||||
|
if (seconds < 60) {
|
||||||
|
return `${Math.round(seconds)} seconds`;
|
||||||
|
} else {
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
const remainingSeconds = Math.round(seconds % 60);
|
||||||
|
return `${minutes}m ${remainingSeconds}s`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate token costs
|
||||||
|
const calculateTokenCost = (promptLength: number): number => {
|
||||||
|
const estimatedTokens = Math.round(promptLength / 4);
|
||||||
|
return (estimatedTokens / 1_000_000) * costPerMillionTokens * 100; // In cents
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use the prompt length from the current prompt
|
||||||
|
const promptLength = currentPrompt.prompt ? currentPrompt.prompt.length : 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Current Prompt Dialog */}
|
{/* Current Prompt Dialog with Debug Info */}
|
||||||
<Dialog
|
<Dialog
|
||||||
open={currentPrompt.isOpen}
|
open={currentPrompt.isOpen}
|
||||||
onOpenChange={(open) => setCurrentPrompt(prev => ({ ...prev, isOpen: open }))}
|
onOpenChange={(open) =>
|
||||||
|
setCurrentPrompt((prev) => ({ ...prev, isOpen: open }))
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<DialogContent className="max-w-4xl h-[80vh]">
|
<DialogContent className="max-w-4xl max-h-[90vh] overflow-hidden">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Current AI Prompt</DialogTitle>
|
<DialogTitle>Current AI Prompt</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
This is the exact prompt that would be sent to the AI for validation
|
This is the current prompt that would be sent to the AI for
|
||||||
|
validation
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<ScrollArea className="flex-1">
|
|
||||||
{currentPrompt.isLoading ? (
|
<div className="flex flex-col h-[calc(90vh-120px)] overflow-hidden">
|
||||||
<div className="flex items-center justify-center h-full">
|
{/* Debug Information Section */}
|
||||||
<Loader2 className="h-8 w-8 animate-spin" />
|
<div className="mb-4 flex-shrink-0">
|
||||||
</div>
|
{currentPrompt.isLoading ? (
|
||||||
) : (
|
<div className="flex justify-center items-center h-[100px]"></div>
|
||||||
<Code className="whitespace-pre-wrap p-4">{currentPrompt.prompt}</Code>
|
) : (
|
||||||
)}
|
<div className="grid grid-cols-3 gap-4">
|
||||||
</ScrollArea>
|
<Card className="py-2">
|
||||||
|
<CardHeader className="py-2">
|
||||||
|
<CardTitle className="text-base">Prompt Length</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="py-2">
|
||||||
|
<div className="flex flex-col space-y-2">
|
||||||
|
<div className="text-sm">
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
Characters:
|
||||||
|
</span>{" "}
|
||||||
|
<span className="font-semibold">{promptLength}</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm">
|
||||||
|
<span className="text-muted-foreground">Tokens:</span>{" "}
|
||||||
|
<span className="font-semibold">
|
||||||
|
~{Math.round(promptLength / 4)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="py-2">
|
||||||
|
<CardHeader className="py-2">
|
||||||
|
<CardTitle className="text-base">Cost Estimate</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="py-2">
|
||||||
|
<div className="flex flex-col space-y-2">
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<label
|
||||||
|
htmlFor="costPerMillion"
|
||||||
|
className="text-sm text-muted-foreground"
|
||||||
|
>
|
||||||
|
$
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="costPerMillion"
|
||||||
|
className="w-[40px] px-1 border rounded-md text-sm"
|
||||||
|
defaultValue={costPerMillionTokens.toFixed(2)}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = parseFloat(e.target.value);
|
||||||
|
if (!isNaN(value)) {
|
||||||
|
setCostPerMillionTokens(value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor="costPerMillion"
|
||||||
|
className="text-sm text-muted-foreground ml-1"
|
||||||
|
>
|
||||||
|
per million input tokens
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm">
|
||||||
|
<span className="text-muted-foreground">Cost:</span>{" "}
|
||||||
|
<span className="font-semibold">
|
||||||
|
{calculateTokenCost(promptLength).toFixed(1)}¢
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="py-2">
|
||||||
|
<CardHeader className="py-2">
|
||||||
|
<CardTitle className="text-base">
|
||||||
|
Processing Time
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="py-2">
|
||||||
|
<div className="flex flex-col space-y-2">
|
||||||
|
{debugData?.estimatedProcessingTime ? (
|
||||||
|
debugData.estimatedProcessingTime.seconds ? (
|
||||||
|
<>
|
||||||
|
<div className="text-sm">
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
Estimated time:
|
||||||
|
</span>{" "}
|
||||||
|
<span className="font-semibold">
|
||||||
|
{formatTime(
|
||||||
|
debugData.estimatedProcessingTime.seconds
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
Based on{" "}
|
||||||
|
{debugData.estimatedProcessingTime.sampleCount}{" "}
|
||||||
|
similar validation
|
||||||
|
{debugData.estimatedProcessingTime
|
||||||
|
.sampleCount !== 1
|
||||||
|
? "s"
|
||||||
|
: ""}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
No historical data available for this prompt size
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
No processing time data available
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Prompt Section */}
|
||||||
|
<div className="flex-1 min-h-0">
|
||||||
|
<ScrollArea className="h-full w-full">
|
||||||
|
{currentPrompt.isLoading ? (
|
||||||
|
<div className="flex items-center justify-center h-full">
|
||||||
|
<Loader2 className="h-8 w-8 animate-spin" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Code className="whitespace-pre-wrap p-4 break-normal max-w-full">
|
||||||
|
{currentPrompt.prompt}
|
||||||
|
</Code>
|
||||||
|
)}
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* AI Validation Progress Dialog */}
|
{/* AI Validation Progress Dialog */}
|
||||||
<Dialog
|
<Dialog
|
||||||
open={aiValidationProgress.isOpen}
|
open={aiValidationProgress.isOpen}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
// Only allow closing if validation failed
|
// Only allow closing if validation failed
|
||||||
if (!open && aiValidationProgress.step === -1) {
|
if (!open && aiValidationProgress.step === -1) {
|
||||||
setAiValidationProgress(prev => ({ ...prev, isOpen: false }));
|
setAiValidationProgress((prev) => ({ ...prev, isOpen: false }));
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -76,17 +276,28 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
|
|||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="h-2 w-full bg-secondary rounded-full overflow-hidden">
|
<div className="h-2 w-full bg-secondary rounded-full overflow-hidden">
|
||||||
<div
|
<div
|
||||||
className="h-full bg-primary transition-all duration-500"
|
className="h-full bg-primary transition-all duration-500"
|
||||||
style={{
|
style={{
|
||||||
width: `${aiValidationProgress.progressPercent ?? Math.round((aiValidationProgress.step / 5) * 100)}%`,
|
width: `${
|
||||||
backgroundColor: aiValidationProgress.step === -1 ? 'var(--destructive)' : undefined
|
aiValidationProgress.progressPercent ??
|
||||||
|
Math.round((aiValidationProgress.step / 5) * 100)
|
||||||
|
}%`,
|
||||||
|
backgroundColor:
|
||||||
|
aiValidationProgress.step === -1
|
||||||
|
? "var(--destructive)"
|
||||||
|
: undefined,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-muted-foreground w-12 text-right">
|
<div className="text-sm text-muted-foreground w-12 text-right">
|
||||||
{aiValidationProgress.step === -1 ? '❌' : `${aiValidationProgress.progressPercent ?? Math.round((aiValidationProgress.step / 5) * 100)}%`}
|
{aiValidationProgress.step === -1
|
||||||
|
? "❌"
|
||||||
|
: `${
|
||||||
|
aiValidationProgress.progressPercent ??
|
||||||
|
Math.round((aiValidationProgress.step / 5) * 100)
|
||||||
|
}%`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-center text-sm text-muted-foreground">
|
<p className="text-center text-sm text-muted-foreground">
|
||||||
@@ -94,32 +305,43 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
|
|||||||
</p>
|
</p>
|
||||||
{(() => {
|
{(() => {
|
||||||
// Only show time remaining if we have an estimate and are in progress
|
// Only show time remaining if we have an estimate and are in progress
|
||||||
return aiValidationProgress.estimatedSeconds &&
|
return (
|
||||||
aiValidationProgress.elapsedSeconds !== undefined &&
|
aiValidationProgress.estimatedSeconds &&
|
||||||
aiValidationProgress.step > 0 &&
|
aiValidationProgress.elapsedSeconds !== undefined &&
|
||||||
|
aiValidationProgress.step > 0 &&
|
||||||
aiValidationProgress.step < 5 && (
|
aiValidationProgress.step < 5 && (
|
||||||
<div className="text-center text-sm">
|
<div className="text-center text-sm">
|
||||||
{(() => {
|
{(() => {
|
||||||
// Calculate time remaining using the elapsed seconds
|
// Calculate time remaining using the elapsed seconds
|
||||||
const elapsedSeconds = aiValidationProgress.elapsedSeconds;
|
const elapsedSeconds =
|
||||||
const totalEstimatedSeconds = aiValidationProgress.estimatedSeconds;
|
aiValidationProgress.elapsedSeconds;
|
||||||
const remainingSeconds = Math.max(0, totalEstimatedSeconds - elapsedSeconds);
|
const totalEstimatedSeconds =
|
||||||
|
aiValidationProgress.estimatedSeconds;
|
||||||
// Format time remaining
|
const remainingSeconds = Math.max(
|
||||||
if (remainingSeconds < 60) {
|
0,
|
||||||
return `Approximately ${Math.round(remainingSeconds)} seconds remaining`;
|
totalEstimatedSeconds - elapsedSeconds
|
||||||
} else {
|
);
|
||||||
const minutes = Math.floor(remainingSeconds / 60);
|
|
||||||
const seconds = Math.round(remainingSeconds % 60);
|
// Format time remaining
|
||||||
return `Approximately ${minutes}m ${seconds}s remaining`;
|
if (remainingSeconds < 60) {
|
||||||
}
|
return `Approximately ${Math.round(
|
||||||
})()}
|
remainingSeconds
|
||||||
{aiValidationProgress.promptLength && (
|
)} seconds remaining`;
|
||||||
<p className="mt-1 text-xs text-muted-foreground">
|
} else {
|
||||||
Prompt length: {aiValidationProgress.promptLength.toLocaleString()} characters
|
const minutes = Math.floor(remainingSeconds / 60);
|
||||||
</p>
|
const seconds = Math.round(remainingSeconds % 60);
|
||||||
)}
|
return `Approximately ${minutes}m ${seconds}s remaining`;
|
||||||
</div>
|
}
|
||||||
|
})()}
|
||||||
|
{aiValidationProgress.promptLength && (
|
||||||
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
|
Prompt length:{" "}
|
||||||
|
{aiValidationProgress.promptLength.toLocaleString()}{" "}
|
||||||
|
characters
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
@@ -127,9 +349,11 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* AI Validation Results Dialog */}
|
{/* AI Validation Results Dialog */}
|
||||||
<Dialog
|
<Dialog
|
||||||
open={aiValidationDetails.isOpen}
|
open={aiValidationDetails.isOpen}
|
||||||
onOpenChange={(open) => setAiValidationDetails(prev => ({ ...prev, isOpen: open }))}
|
onOpenChange={(open) =>
|
||||||
|
setAiValidationDetails((prev) => ({ ...prev, isOpen: open }))
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<DialogContent className="max-w-4xl">
|
<DialogContent className="max-w-4xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
@@ -139,14 +363,19 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<ScrollArea className="max-h-[60vh]">
|
<ScrollArea className="max-h-[60vh]">
|
||||||
{aiValidationDetails.changeDetails && aiValidationDetails.changeDetails.length > 0 ? (
|
{aiValidationDetails.changeDetails &&
|
||||||
|
aiValidationDetails.changeDetails.length > 0 ? (
|
||||||
<div className="mb-6 space-y-6">
|
<div className="mb-6 space-y-6">
|
||||||
<h3 className="font-semibold text-lg">Detailed Changes:</h3>
|
<h3 className="font-semibold text-lg">Detailed Changes:</h3>
|
||||||
{aiValidationDetails.changeDetails.map((product, i) => {
|
{aiValidationDetails.changeDetails.map((product, i) => {
|
||||||
// Find the title change if it exists
|
// Find the title change if it exists
|
||||||
const titleChange = product.changes.find(c => c.field === 'title');
|
const titleChange = product.changes.find(
|
||||||
const titleValue = titleChange ? titleChange.corrected : product.title;
|
(c) => c.field === "title"
|
||||||
|
);
|
||||||
|
const titleValue = titleChange
|
||||||
|
? titleChange.corrected
|
||||||
|
: product.title;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={`product-${i}`} className="border rounded-md p-4">
|
<div key={`product-${i}`} className="border rounded-md p-4">
|
||||||
<h4 className="font-medium text-base mb-3">
|
<h4 className="font-medium text-base mb-3">
|
||||||
@@ -163,29 +392,43 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
|
|||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{product.changes.map((change, j) => {
|
{product.changes.map((change, j) => {
|
||||||
const field = fields.find(f => f.key === change.field);
|
const field = fields.find(
|
||||||
const fieldLabel = field ? field.label : change.field;
|
(f) => f.key === change.field
|
||||||
const isReverted = isChangeReverted(product.productIndex, change.field);
|
|
||||||
|
|
||||||
// Get highlighted differences
|
|
||||||
const { originalHtml, correctedHtml } = getFieldDisplayValueWithHighlight(
|
|
||||||
change.field,
|
|
||||||
change.original,
|
|
||||||
change.corrected
|
|
||||||
);
|
);
|
||||||
|
const fieldLabel = field
|
||||||
|
? field.label
|
||||||
|
: change.field;
|
||||||
|
const isReverted = isChangeReverted(
|
||||||
|
product.productIndex,
|
||||||
|
change.field
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get highlighted differences
|
||||||
|
const { originalHtml, correctedHtml } =
|
||||||
|
getFieldDisplayValueWithHighlight(
|
||||||
|
change.field,
|
||||||
|
change.original,
|
||||||
|
change.corrected
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow key={`change-${j}`}>
|
<TableRow key={`change-${j}`}>
|
||||||
<TableCell className="font-medium">{fieldLabel}</TableCell>
|
<TableCell className="font-medium">
|
||||||
|
{fieldLabel}
|
||||||
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div
|
<div
|
||||||
dangerouslySetInnerHTML={{ __html: originalHtml }}
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: originalHtml,
|
||||||
|
}}
|
||||||
className={isReverted ? "font-medium" : ""}
|
className={isReverted ? "font-medium" : ""}
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div
|
<div
|
||||||
dangerouslySetInnerHTML={{ __html: correctedHtml }}
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: correctedHtml,
|
||||||
|
}}
|
||||||
className={!isReverted ? "font-medium" : ""}
|
className={!isReverted ? "font-medium" : ""}
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -207,7 +450,10 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// Call the revert function directly
|
// Call the revert function directly
|
||||||
revertAiChange(product.productIndex, change.field);
|
revertAiChange(
|
||||||
|
product.productIndex,
|
||||||
|
change.field
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Revert Change
|
Revert Change
|
||||||
@@ -226,12 +472,17 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="py-8 text-center text-muted-foreground">
|
<div className="py-8 text-center text-muted-foreground">
|
||||||
{aiValidationDetails.warnings && aiValidationDetails.warnings.length > 0 ? (
|
{aiValidationDetails.warnings &&
|
||||||
|
aiValidationDetails.warnings.length > 0 ? (
|
||||||
<div>
|
<div>
|
||||||
<p className="mb-4">No changes were made, but the AI provided some warnings:</p>
|
<p className="mb-4">
|
||||||
|
No changes were made, but the AI provided some warnings:
|
||||||
|
</p>
|
||||||
<ul className="list-disc pl-8 text-left">
|
<ul className="list-disc pl-8 text-left">
|
||||||
{aiValidationDetails.warnings.map((warning, i) => (
|
{aiValidationDetails.warnings.map((warning, i) => (
|
||||||
<li key={`warning-${i}`} className="mb-2">{warning}</li>
|
<li key={`warning-${i}`} className="mb-2">
|
||||||
|
{warning}
|
||||||
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -245,4 +496,4 @@ export const AiValidationDialogs: React.FC<AiValidationDialogsProps> = ({
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1200,6 +1200,7 @@ const ValidationContainer = <T extends string>({
|
|||||||
isChangeReverted={aiValidation.isChangeReverted}
|
isChangeReverted={aiValidation.isChangeReverted}
|
||||||
getFieldDisplayValueWithHighlight={aiValidation.getFieldDisplayValueWithHighlight}
|
getFieldDisplayValueWithHighlight={aiValidation.getFieldDisplayValueWithHighlight}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
|
debugData={aiValidation.currentPrompt.debugData}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Product Search Dialog */}
|
{/* Product Search Dialog */}
|
||||||
|
|||||||
@@ -42,6 +42,25 @@ export interface CurrentPrompt {
|
|||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
prompt: string | null;
|
prompt: string | null;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
debugData?: {
|
||||||
|
taxonomyStats: {
|
||||||
|
categories: number;
|
||||||
|
themes: number;
|
||||||
|
colors: number;
|
||||||
|
taxCodes: number;
|
||||||
|
sizeCategories: number;
|
||||||
|
suppliers: number;
|
||||||
|
companies: number;
|
||||||
|
artists: number;
|
||||||
|
} | null;
|
||||||
|
basePrompt: string;
|
||||||
|
sampleFullPrompt: string;
|
||||||
|
promptLength: number;
|
||||||
|
estimatedProcessingTime?: {
|
||||||
|
seconds: number | null;
|
||||||
|
sampleCount: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Declare global interface for the timer
|
// Declare global interface for the timer
|
||||||
@@ -250,7 +269,11 @@ export const useAiValidation = <T extends string>(
|
|||||||
// Function to show current prompt
|
// Function to show current prompt
|
||||||
const showCurrentPrompt = useCallback(async () => {
|
const showCurrentPrompt = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
setCurrentPrompt(prev => ({ ...prev, isLoading: true, isOpen: true }));
|
setCurrentPrompt(prev => ({
|
||||||
|
...prev,
|
||||||
|
isLoading: true,
|
||||||
|
isOpen: true
|
||||||
|
}));
|
||||||
|
|
||||||
// Debug log the data being sent
|
// Debug log the data being sent
|
||||||
console.log('Sending products data:', {
|
console.log('Sending products data:', {
|
||||||
@@ -272,7 +295,7 @@ export const useAiValidation = <T extends string>(
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Use POST to send products in request body
|
// Use POST to send products in request body
|
||||||
const response = await fetch(`${getApiUrl()}/ai-validation/debug`, {
|
const response = await fetch(`${await getApiUrl()}/ai-validation/debug`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -294,7 +317,14 @@ export const useAiValidation = <T extends string>(
|
|||||||
setCurrentPrompt(prev => ({
|
setCurrentPrompt(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
prompt: promptContent,
|
prompt: promptContent,
|
||||||
isLoading: false
|
isLoading: false,
|
||||||
|
debugData: {
|
||||||
|
taxonomyStats: result.taxonomyStats || null,
|
||||||
|
basePrompt: result.basePrompt || '',
|
||||||
|
sampleFullPrompt: result.sampleFullPrompt || '',
|
||||||
|
promptLength: result.promptLength || (promptContent ? promptContent.length : 0),
|
||||||
|
estimatedProcessingTime: result.estimatedProcessingTime
|
||||||
|
}
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
throw new Error('No prompt returned from server');
|
throw new Error('No prompt returned from server');
|
||||||
|
|||||||
@@ -133,8 +133,9 @@ export function PerformanceMetrics() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function getCategoryName(_cat_id: number): import("react").ReactNode {
|
function getCategoryName(cat_id: number): import("react").ReactNode {
|
||||||
throw new Error('Function not implemented.');
|
// Simple implementation that just returns the ID as a string
|
||||||
|
return `Category ${cat_id}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -217,15 +218,19 @@ export function PerformanceMetrics() {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{abcConfigs.map((config) => (
|
{abcConfigs && abcConfigs.length > 0 ? abcConfigs.map((config) => (
|
||||||
<TableRow key={`${config.cat_id}-${config.vendor}`}>
|
<TableRow key={`${config.cat_id}-${config.vendor}`}>
|
||||||
<TableCell>{config.cat_id ? getCategoryName(config.cat_id) : 'Global'}</TableCell>
|
<TableCell>{config.cat_id ? getCategoryName(config.cat_id) : 'Global'}</TableCell>
|
||||||
<TableCell>{config.vendor || 'All Vendors'}</TableCell>
|
<TableCell>{config.vendor || 'All Vendors'}</TableCell>
|
||||||
<TableCell className="text-right">{config.a_threshold}%</TableCell>
|
<TableCell className="text-right">{config.a_threshold !== undefined ? `${config.a_threshold}%` : '0%'}</TableCell>
|
||||||
<TableCell className="text-right">{config.b_threshold}%</TableCell>
|
<TableCell className="text-right">{config.b_threshold !== undefined ? `${config.b_threshold}%` : '0%'}</TableCell>
|
||||||
<TableCell className="text-right">{config.classification_period_days}</TableCell>
|
<TableCell className="text-right">{config.classification_period_days || 0}</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
)) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={5} className="text-center py-4">No ABC configurations available</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
<Button onClick={handleUpdateABCConfig}>
|
<Button onClick={handleUpdateABCConfig}>
|
||||||
@@ -253,14 +258,26 @@ export function PerformanceMetrics() {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{turnoverConfigs.map((config) => (
|
{turnoverConfigs && turnoverConfigs.length > 0 ? turnoverConfigs.map((config) => (
|
||||||
<TableRow key={`${config.cat_id}-${config.vendor}`}>
|
<TableRow key={`${config.cat_id}-${config.vendor}`}>
|
||||||
<TableCell>{config.cat_id ? getCategoryName(config.cat_id) : 'Global'}</TableCell>
|
<TableCell>{config.cat_id ? getCategoryName(config.cat_id) : 'Global'}</TableCell>
|
||||||
<TableCell>{config.vendor || 'All Vendors'}</TableCell>
|
<TableCell>{config.vendor || 'All Vendors'}</TableCell>
|
||||||
<TableCell className="text-right">{config.calculation_period_days}</TableCell>
|
<TableCell className="text-right">{config.calculation_period_days}</TableCell>
|
||||||
<TableCell className="text-right">{config.target_rate.toFixed(2)}</TableCell>
|
<TableCell className="text-right">
|
||||||
|
{config.target_rate !== undefined && config.target_rate !== null
|
||||||
|
? (typeof config.target_rate === 'number'
|
||||||
|
? config.target_rate.toFixed(2)
|
||||||
|
: (isNaN(parseFloat(String(config.target_rate)))
|
||||||
|
? '0.00'
|
||||||
|
: parseFloat(String(config.target_rate)).toFixed(2)))
|
||||||
|
: '0.00'}
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
)) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={4} className="text-center py-4">No turnover configurations available</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
<Button onClick={handleUpdateTurnoverConfig}>
|
<Button onClick={handleUpdateTurnoverConfig}>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect, useContext } from "react";
|
import { useState, useEffect, useContext } from "react";
|
||||||
import { Card, CardContent, CardDescription, 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";
|
||||||
import { UserList } from "./UserList";
|
import { UserList } from "./UserList";
|
||||||
@@ -32,7 +32,7 @@ interface PermissionCategory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function UserManagement() {
|
export function UserManagement() {
|
||||||
const { token, fetchCurrentUser, user } = useContext(AuthContext);
|
const { token, fetchCurrentUser } = useContext(AuthContext);
|
||||||
const [users, setUsers] = useState<User[]>([]);
|
const [users, setUsers] = useState<User[]>([]);
|
||||||
const [selectedUser, setSelectedUser] = useState<User | null>(null);
|
const [selectedUser, setSelectedUser] = useState<User | null>(null);
|
||||||
const [isAddingUser, setIsAddingUser] = useState(false);
|
const [isAddingUser, setIsAddingUser] = useState(false);
|
||||||
@@ -199,7 +199,7 @@ export function UserManagement() {
|
|||||||
// Check if permissions are objects (from the form) and convert to IDs for the API
|
// Check if permissions are objects (from the form) and convert to IDs for the API
|
||||||
if (userData.permissions.length > 0 && typeof userData.permissions[0] === 'object') {
|
if (userData.permissions.length > 0 && typeof userData.permissions[0] === 'object') {
|
||||||
// The backend expects permission IDs, not just the code strings
|
// The backend expects permission IDs, not just the code strings
|
||||||
formattedUserData.permissions = userData.permissions.map(p => p.id);
|
formattedUserData.permissions = userData.permissions.map((p: { id: any; }) => p.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,9 +334,6 @@ export function UserManagement() {
|
|||||||
<CardHeader className="flex flex-row items-center justify-between">
|
<CardHeader className="flex flex-row items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<CardTitle>User Management</CardTitle>
|
<CardTitle>User Management</CardTitle>
|
||||||
<CardDescription>
|
|
||||||
Manage users and their permissions
|
|
||||||
</CardDescription>
|
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={handleAddUser}>
|
<Button onClick={handleAddUser}>
|
||||||
Add User
|
Add User
|
||||||
|
|||||||
@@ -1,200 +0,0 @@
|
|||||||
import { useEffect, useState } from "react"
|
|
||||||
import { Button } from "@/components/ui/button"
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
|
||||||
import { Code } from "@/components/ui/code"
|
|
||||||
import { useToast } from "@/hooks/use-toast"
|
|
||||||
import { Loader2 } from "lucide-react"
|
|
||||||
import config from "@/config"
|
|
||||||
|
|
||||||
interface TaxonomyStats {
|
|
||||||
categories: number
|
|
||||||
themes: number
|
|
||||||
colors: number
|
|
||||||
taxCodes: number
|
|
||||||
sizeCategories: number
|
|
||||||
suppliers: number
|
|
||||||
companies: number
|
|
||||||
artists: number
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DebugData {
|
|
||||||
taxonomyStats: TaxonomyStats | null
|
|
||||||
basePrompt: string
|
|
||||||
sampleFullPrompt: string
|
|
||||||
promptLength: number
|
|
||||||
estimatedProcessingTime?: {
|
|
||||||
seconds: number | null
|
|
||||||
sampleCount: number
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AiValidationDebug() {
|
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
|
||||||
const [debugData, setDebugData] = useState<DebugData | null>(null)
|
|
||||||
const { toast } = useToast()
|
|
||||||
|
|
||||||
const fetchDebugData = async () => {
|
|
||||||
setIsLoading(true)
|
|
||||||
try {
|
|
||||||
// Use a sample product to avoid loading full taxonomy
|
|
||||||
const sampleProduct = {
|
|
||||||
title: "Sample Product",
|
|
||||||
description: "A sample product for testing",
|
|
||||||
SKU: "SAMPLE-001",
|
|
||||||
price: "9.99",
|
|
||||||
cost_each: "5.00",
|
|
||||||
qty_per_unit: "1",
|
|
||||||
case_qty: "12"
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(`${config.apiUrl}/ai-validation/debug`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ products: [sampleProduct] })
|
|
||||||
})
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Failed to fetch debug data')
|
|
||||||
}
|
|
||||||
const data = await response.json()
|
|
||||||
setDebugData(data)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching debug data:', error)
|
|
||||||
toast({
|
|
||||||
variant: "destructive",
|
|
||||||
title: "Error",
|
|
||||||
description: error instanceof Error ? error.message : "Failed to fetch debug data"
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchDebugData()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="container mx-auto py-6 space-y-6">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<h1 className="text-3xl font-bold tracking-tight">AI Validation Debug</h1>
|
|
||||||
<div className="space-x-4">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={fetchDebugData}
|
|
||||||
disabled={isLoading}
|
|
||||||
>
|
|
||||||
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
||||||
Refresh Data
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{debugData && (
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Taxonomy Stats</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
{debugData.taxonomyStats ? (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div>Categories: {debugData.taxonomyStats.categories}</div>
|
|
||||||
<div>Themes: {debugData.taxonomyStats.themes}</div>
|
|
||||||
<div>Colors: {debugData.taxonomyStats.colors}</div>
|
|
||||||
<div>Tax Codes: {debugData.taxonomyStats.taxCodes}</div>
|
|
||||||
<div>Size Categories: {debugData.taxonomyStats.sizeCategories}</div>
|
|
||||||
<div>Suppliers: {debugData.taxonomyStats.suppliers}</div>
|
|
||||||
<div>Companies: {debugData.taxonomyStats.companies}</div>
|
|
||||||
<div>Artists: {debugData.taxonomyStats.artists}</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div>No taxonomy data available</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Prompt Length</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div>Characters: {debugData.promptLength}</div>
|
|
||||||
<div>Tokens (est.): ~{Math.round(debugData.promptLength / 4)}</div>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label htmlFor="costPerMillion" className="text-sm text-muted-foreground">
|
|
||||||
Cost per million tokens ($)
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="costPerMillion"
|
|
||||||
type="number"
|
|
||||||
className="w-full px-3 py-2 border rounded-md"
|
|
||||||
defaultValue="2.50"
|
|
||||||
onChange={(e) => {
|
|
||||||
const costPerMillion = parseFloat(e.target.value)
|
|
||||||
if (!isNaN(costPerMillion)) {
|
|
||||||
const tokens = Math.round(debugData.promptLength / 4)
|
|
||||||
const cost = (tokens / 1_000_000) * costPerMillion * 100 // Convert to cents
|
|
||||||
const costElement = document.getElementById('tokenCost')
|
|
||||||
if (costElement) {
|
|
||||||
costElement.textContent = cost.toFixed(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className="text-sm">
|
|
||||||
Cost: <span id="tokenCost">{((Math.round(debugData.promptLength / 4) / 1_000_000) * 3 * 100).toFixed(1)}</span>¢
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{debugData.estimatedProcessingTime && (
|
|
||||||
<div className="mt-4 p-3 bg-muted rounded-md">
|
|
||||||
<h3 className="text-sm font-medium mb-2">Processing Time Estimate</h3>
|
|
||||||
{debugData.estimatedProcessingTime.seconds ? (
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="text-sm">
|
|
||||||
Estimated time: {formatTime(debugData.estimatedProcessingTime.seconds)}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-muted-foreground">
|
|
||||||
Based on {debugData.estimatedProcessingTime.sampleCount} similar validation{debugData.estimatedProcessingTime.sampleCount !== 1 ? 's' : ''}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="text-sm text-muted-foreground">No historical data available for this prompt size</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="col-span-full">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Full Sample Prompt</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<ScrollArea className="h-[500px] w-full rounded-md border p-4">
|
|
||||||
<Code className="whitespace-pre-wrap">{debugData.sampleFullPrompt}</Code>
|
|
||||||
</ScrollArea>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to format time in a human-readable way
|
|
||||||
function formatTime(seconds: number): string {
|
|
||||||
if (seconds < 60) {
|
|
||||||
return `${Math.round(seconds)} seconds`;
|
|
||||||
} else {
|
|
||||||
const minutes = Math.floor(seconds / 60);
|
|
||||||
const remainingSeconds = Math.round(seconds % 60);
|
|
||||||
return `${minutes}m ${remainingSeconds}s`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,84 +8,229 @@ import { UserManagement } from "@/components/settings/UserManagement";
|
|||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
import { Protected } from "@/components/auth/Protected";
|
import { Protected } from "@/components/auth/Protected";
|
||||||
|
import { useContext, useMemo } from "react";
|
||||||
|
import { AuthContext } from "@/contexts/AuthContext";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
|
||||||
|
// Define types for settings structure
|
||||||
|
interface SettingsTab {
|
||||||
|
id: string;
|
||||||
|
permission: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SettingsGroup {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
tabs: SettingsTab[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define available settings tabs with their permission requirements and groups
|
||||||
|
const SETTINGS_GROUPS: SettingsGroup[] = [
|
||||||
|
{
|
||||||
|
id: "inventory",
|
||||||
|
label: "Inventory Settings",
|
||||||
|
tabs: [
|
||||||
|
{ id: "stock-management", permission: "settings:stock_management", label: "Stock Management" },
|
||||||
|
{ id: "performance-metrics", permission: "settings:performance_metrics", label: "Performance Metrics" },
|
||||||
|
{ id: "calculation-settings", permission: "settings:calculation_settings", label: "Calculation Settings" },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "content",
|
||||||
|
label: "Content Management",
|
||||||
|
tabs: [
|
||||||
|
{ id: "templates", permission: "settings:templates", label: "Template Management" },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "system",
|
||||||
|
label: "System",
|
||||||
|
tabs: [
|
||||||
|
{ id: "user-management", permission: "settings:user_management", label: "User Management" },
|
||||||
|
{ id: "data-management", permission: "settings:data_management", label: "Data Management" },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Flatten tabs for easier access
|
||||||
|
const SETTINGS_TABS = SETTINGS_GROUPS.flatMap(group => group.tabs);
|
||||||
|
|
||||||
export function Settings() {
|
export function Settings() {
|
||||||
|
const { user } = useContext(AuthContext);
|
||||||
|
|
||||||
|
// Determine the first tab the user has access to
|
||||||
|
const defaultTab = useMemo(() => {
|
||||||
|
// Admin users have access to all tabs
|
||||||
|
if (user?.is_admin) {
|
||||||
|
return SETTINGS_TABS[0].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the first tab the user has permission to access
|
||||||
|
const firstAccessibleTab = SETTINGS_TABS.find(tab =>
|
||||||
|
user?.permissions?.includes(tab.permission)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Return the ID of the first accessible tab, or first tab as fallback
|
||||||
|
return firstAccessibleTab?.id || SETTINGS_TABS[0].id;
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
|
// Check if user has access to any tab
|
||||||
|
const hasAccessToAnyTab = useMemo(() => {
|
||||||
|
if (user?.is_admin) return true;
|
||||||
|
return SETTINGS_TABS.some(tab => user?.permissions?.includes(tab.permission));
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
|
// If user doesn't have access to any tabs, show a helpful message
|
||||||
|
if (!hasAccessToAnyTab) {
|
||||||
|
return (
|
||||||
|
<motion.div layout className="container mx-auto py-6">
|
||||||
|
<div className="mb-6">
|
||||||
|
<h1 className="text-3xl font-bold">Settings</h1>
|
||||||
|
</div>
|
||||||
|
<Alert>
|
||||||
|
<AlertDescription>
|
||||||
|
You don't have permission to access any settings. Please contact an administrator for assistance.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to check if the user has access to any tab in a group
|
||||||
|
const hasAccessToGroup = (group: SettingsGroup): boolean => {
|
||||||
|
if (user?.is_admin) return true;
|
||||||
|
return group.tabs.some(tab => user?.permissions?.includes(tab.permission));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div layout className="container mx-auto py-6">
|
<motion.div layout className="container mx-auto py-6">
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<h1 className="text-3xl font-bold">Settings</h1>
|
<h1 className="text-3xl font-bold">Settings</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Tabs defaultValue="data-management" className="space-y-4">
|
<Tabs defaultValue={defaultTab} orientation="vertical" className="flex flex-row min-h-[500px]">
|
||||||
<TabsList>
|
<div className="w-60 border-r pr-8">
|
||||||
<TabsTrigger value="data-management">Data Management</TabsTrigger>
|
<TabsList className="flex flex-col h-auto justify-start items-stretch p-0 bg-transparent">
|
||||||
<TabsTrigger value="stock-management">Stock Management</TabsTrigger>
|
{SETTINGS_GROUPS.map((group) => (
|
||||||
<TabsTrigger value="performance-metrics">
|
hasAccessToGroup(group) && (
|
||||||
Performance Metrics
|
<div key={group.id} className="">
|
||||||
</TabsTrigger>
|
<h3 className="font-semibold text-sm px-3 py-2 bg-muted border text-foreground rounded-md mb-2">
|
||||||
<Protected permission="edit:system_settings">
|
{group.label}
|
||||||
<TabsTrigger value="calculation-settings">
|
</h3>
|
||||||
Calculation Settings
|
<div className="space-y-1 pl-1">
|
||||||
</TabsTrigger>
|
{group.tabs.map((tab) => (
|
||||||
</Protected>
|
<Protected key={tab.id} permission={tab.permission}>
|
||||||
<TabsTrigger value="templates">
|
<TabsTrigger
|
||||||
Template Management
|
value={tab.id}
|
||||||
</TabsTrigger>
|
className="w-full justify-start px-3 py-2 text-sm font-normal text-muted-foreground data-[state=active]:font-medium data-[state=active]:text-accent-foreground data-[state=active]:shadow-none rounded-md data-[state=active]:underline"
|
||||||
<Protected
|
>
|
||||||
permission="view:users"
|
{tab.label}
|
||||||
fallback={null}
|
</TabsTrigger>
|
||||||
>
|
</Protected>
|
||||||
<TabsTrigger value="user-management">
|
))}
|
||||||
User Management
|
</div>
|
||||||
</TabsTrigger>
|
{/* Only add separator if not the last group */}
|
||||||
</Protected>
|
{group.id !== SETTINGS_GROUPS[SETTINGS_GROUPS.length - 1].id && (
|
||||||
</TabsList>
|
<Separator className="mt-4 mb-4 opacity-70" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
</div>
|
||||||
|
|
||||||
<TabsContent value="data-management">
|
<div className="pl-8 w-full">
|
||||||
<DataManagement />
|
<TabsContent value="data-management" className="mt-0 focus-visible:outline-none focus-visible:ring-0">
|
||||||
</TabsContent>
|
<Protected
|
||||||
|
permission="settings:data_management"
|
||||||
|
fallback={
|
||||||
|
<Alert>
|
||||||
|
<AlertDescription>
|
||||||
|
You don't have permission to access Data Management.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<DataManagement />
|
||||||
|
</Protected>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="stock-management">
|
<TabsContent value="stock-management" className="mt-0 focus-visible:outline-none focus-visible:ring-0">
|
||||||
<StockManagement />
|
<Protected
|
||||||
</TabsContent>
|
permission="settings:stock_management"
|
||||||
|
fallback={
|
||||||
|
<Alert>
|
||||||
|
<AlertDescription>
|
||||||
|
You don't have permission to access Stock Management.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StockManagement />
|
||||||
|
</Protected>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="performance-metrics">
|
<TabsContent value="performance-metrics" className="mt-0 focus-visible:outline-none focus-visible:ring-0">
|
||||||
<PerformanceMetrics />
|
<Protected
|
||||||
</TabsContent>
|
permission="settings:performance_metrics"
|
||||||
|
fallback={
|
||||||
|
<Alert>
|
||||||
|
<AlertDescription>
|
||||||
|
You don't have permission to access Performance Metrics.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<PerformanceMetrics />
|
||||||
|
</Protected>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="calculation-settings">
|
<TabsContent value="calculation-settings" className="mt-0 focus-visible:outline-none focus-visible:ring-0">
|
||||||
<Protected
|
<Protected
|
||||||
permission="edit:system_settings"
|
permission="settings:calculation_settings"
|
||||||
fallback={
|
fallback={
|
||||||
<Alert>
|
<Alert>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
You don't have permission to access Calculation Settings.
|
You don't have permission to access Calculation Settings.
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<CalculationSettings />
|
<CalculationSettings />
|
||||||
</Protected>
|
</Protected>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="templates">
|
<TabsContent value="templates" className="mt-0 focus-visible:outline-none focus-visible:ring-0">
|
||||||
<TemplateManagement />
|
<Protected
|
||||||
</TabsContent>
|
permission="settings:templates"
|
||||||
|
fallback={
|
||||||
<TabsContent value="user-management">
|
<Alert>
|
||||||
<Protected
|
<AlertDescription>
|
||||||
permission="view:users"
|
You don't have permission to access Template Management.
|
||||||
fallback={
|
</AlertDescription>
|
||||||
<Alert>
|
</Alert>
|
||||||
<AlertDescription>
|
}
|
||||||
You don't have permission to access User Management.
|
>
|
||||||
</AlertDescription>
|
<TemplateManagement />
|
||||||
</Alert>
|
</Protected>
|
||||||
}
|
</TabsContent>
|
||||||
>
|
|
||||||
<UserManagement />
|
<TabsContent value="user-management" className="mt-0 focus-visible:outline-none focus-visible:ring-0">
|
||||||
</Protected>
|
<Protected
|
||||||
</TabsContent>
|
permission="settings:user_management"
|
||||||
|
fallback={
|
||||||
|
<Alert>
|
||||||
|
<AlertDescription>
|
||||||
|
You don't have permission to access User Management.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<UserManagement />
|
||||||
|
</Protected>
|
||||||
|
</TabsContent>
|
||||||
|
</div>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user