From 1fcbf549895a2988ac561974339a668999accb29 Mon Sep 17 00:00:00 2001 From: Matt Date: Sun, 23 Mar 2025 22:01:41 -0400 Subject: [PATCH] Layout/style tweaks, fix performance metrics settings page --- inventory/src/App.tsx | 6 - .../src/components/layout/AppSidebar.tsx | 1 + .../ValidationStepNew/ValidationStepNew.tsx | 127 ++++++ .../components/AiValidationDialogs.tsx | 427 ++++++++++++++---- .../components/ValidationContainer.tsx | 1 + .../hooks/useAiValidation.tsx | 36 +- .../settings/PerformanceMetrics.tsx | 37 +- .../components/settings/UserManagement.tsx | 9 +- inventory/src/pages/AiValidationDebug.tsx | 200 -------- inventory/src/pages/Settings.tsx | 288 +++++++----- 10 files changed, 697 insertions(+), 435 deletions(-) create mode 100644 inventory/src/components/product-import/steps/ValidationStepNew/ValidationStepNew.tsx delete mode 100644 inventory/src/pages/AiValidationDebug.tsx diff --git a/inventory/src/App.tsx b/inventory/src/App.tsx index a6868f4..7ede6ab 100644 --- a/inventory/src/App.tsx +++ b/inventory/src/App.tsx @@ -15,7 +15,6 @@ import Forecasting from "@/pages/Forecasting"; import { Vendors } from '@/pages/Vendors'; import { Categories } from '@/pages/Categories'; import { Import } from '@/pages/Import'; -import { AiValidationDebug } from "@/pages/AiValidationDebug" import { AuthProvider } from './contexts/AuthContext'; import { Protected } from './components/auth/Protected'; import { FirstAccessiblePage } from './components/auth/FirstAccessiblePage'; @@ -129,11 +128,6 @@ function App() { } /> - - - - } /> } /> diff --git a/inventory/src/components/layout/AppSidebar.tsx b/inventory/src/components/layout/AppSidebar.tsx index 785961e..95fe654 100644 --- a/inventory/src/components/layout/AppSidebar.tsx +++ b/inventory/src/components/layout/AppSidebar.tsx @@ -160,6 +160,7 @@ export function AppSidebar() { + diff --git a/inventory/src/components/product-import/steps/ValidationStepNew/ValidationStepNew.tsx b/inventory/src/components/product-import/steps/ValidationStepNew/ValidationStepNew.tsx new file mode 100644 index 0000000..c9ad71b --- /dev/null +++ b/inventory/src/components/product-import/steps/ValidationStepNew/ValidationStepNew.tsx @@ -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({ + isOpen: false, + prompt: '', + isLoading: true, + }); + const [isChangeReverted, setIsChangeReverted] = useState(false); + const [fieldData, setFieldData] = useState([]); + + 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 ( +
+ +
+ ); +}; + +export default ValidationStepNew; \ No newline at end of file diff --git a/inventory/src/components/product-import/steps/ValidationStepNew/components/AiValidationDialogs.tsx b/inventory/src/components/product-import/steps/ValidationStepNew/components/AiValidationDialogs.tsx index b15ebcc..a2fd85d 100644 --- a/inventory/src/components/product-import/steps/ValidationStepNew/components/AiValidationDialogs.tsx +++ b/inventory/src/components/product-import/steps/ValidationStepNew/components/AiValidationDialogs.tsx @@ -1,23 +1,72 @@ -import React from 'react'; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } 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 React, { useState } from "react"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, +} 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 { aiValidationProgress: AiValidationProgress; aiValidationDetails: AiValidationDetails; currentPrompt: CurrentPrompt; - setAiValidationProgress: React.Dispatch>; - setAiValidationDetails: React.Dispatch>; + setAiValidationProgress: React.Dispatch< + React.SetStateAction + >; + setAiValidationDetails: React.Dispatch< + React.SetStateAction + >; setCurrentPrompt: React.Dispatch>; revertAiChange: (productIndex: number, fieldKey: string) => void; 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[]; + debugData?: DebugData; } export const AiValidationDialogs: React.FC = ({ @@ -30,41 +79,192 @@ export const AiValidationDialogs: React.FC = ({ revertAiChange, isChangeReverted, 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 ( <> - {/* Current Prompt Dialog */} - setCurrentPrompt(prev => ({ ...prev, isOpen: open }))} + {/* Current Prompt Dialog with Debug Info */} + + setCurrentPrompt((prev) => ({ ...prev, isOpen: open })) + } > - + Current AI Prompt - 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 - - {currentPrompt.isLoading ? ( -
- -
- ) : ( - {currentPrompt.prompt} - )} -
+ +
+ {/* Debug Information Section */} +
+ {currentPrompt.isLoading ? ( +
+ ) : ( +
+ + + Prompt Length + + +
+
+ + Characters: + {" "} + {promptLength} +
+
+ Tokens:{" "} + + ~{Math.round(promptLength / 4)} + +
+
+
+
+ + + + Cost Estimate + + +
+
+ + { + const value = parseFloat(e.target.value); + if (!isNaN(value)) { + setCostPerMillionTokens(value); + } + }} + /> + +
+
+ Cost:{" "} + + {calculateTokenCost(promptLength).toFixed(1)}¢ + +
+
+
+
+ + + + + Processing Time + + + +
+ {debugData?.estimatedProcessingTime ? ( + debugData.estimatedProcessingTime.seconds ? ( + <> +
+ + Estimated time: + {" "} + + {formatTime( + debugData.estimatedProcessingTime.seconds + )} + +
+
+ Based on{" "} + {debugData.estimatedProcessingTime.sampleCount}{" "} + similar validation + {debugData.estimatedProcessingTime + .sampleCount !== 1 + ? "s" + : ""} +
+ + ) : ( +
+ No historical data available for this prompt size +
+ ) + ) : ( +
+ No processing time data available +
+ )} +
+
+
+
+ )} +
+ + {/* Prompt Section */} +
+ + {currentPrompt.isLoading ? ( +
+ +
+ ) : ( + + {currentPrompt.prompt} + + )} +
+
+
{/* AI Validation Progress Dialog */} - { // Only allow closing if validation failed if (!open && aiValidationProgress.step === -1) { - setAiValidationProgress(prev => ({ ...prev, isOpen: false })); + setAiValidationProgress((prev) => ({ ...prev, isOpen: false })); } }} > @@ -76,17 +276,28 @@ export const AiValidationDialogs: React.FC = ({
-
- {aiValidationProgress.step === -1 ? '❌' : `${aiValidationProgress.progressPercent ?? Math.round((aiValidationProgress.step / 5) * 100)}%`} + {aiValidationProgress.step === -1 + ? "❌" + : `${ + aiValidationProgress.progressPercent ?? + Math.round((aiValidationProgress.step / 5) * 100) + }%`}

@@ -94,32 +305,43 @@ export const AiValidationDialogs: React.FC = ({

{(() => { // Only show time remaining if we have an estimate and are in progress - return aiValidationProgress.estimatedSeconds && - aiValidationProgress.elapsedSeconds !== undefined && - aiValidationProgress.step > 0 && + return ( + aiValidationProgress.estimatedSeconds && + aiValidationProgress.elapsedSeconds !== undefined && + aiValidationProgress.step > 0 && aiValidationProgress.step < 5 && ( -
- {(() => { - // Calculate time remaining using the elapsed seconds - const elapsedSeconds = aiValidationProgress.elapsedSeconds; - const totalEstimatedSeconds = aiValidationProgress.estimatedSeconds; - const remainingSeconds = Math.max(0, totalEstimatedSeconds - elapsedSeconds); - - // Format time remaining - if (remainingSeconds < 60) { - return `Approximately ${Math.round(remainingSeconds)} seconds remaining`; - } else { - const minutes = Math.floor(remainingSeconds / 60); - const seconds = Math.round(remainingSeconds % 60); - return `Approximately ${minutes}m ${seconds}s remaining`; - } - })()} - {aiValidationProgress.promptLength && ( -

- Prompt length: {aiValidationProgress.promptLength.toLocaleString()} characters -

- )} -
+
+ {(() => { + // Calculate time remaining using the elapsed seconds + const elapsedSeconds = + aiValidationProgress.elapsedSeconds; + const totalEstimatedSeconds = + aiValidationProgress.estimatedSeconds; + const remainingSeconds = Math.max( + 0, + totalEstimatedSeconds - elapsedSeconds + ); + + // Format time remaining + if (remainingSeconds < 60) { + return `Approximately ${Math.round( + remainingSeconds + )} seconds remaining`; + } else { + const minutes = Math.floor(remainingSeconds / 60); + const seconds = Math.round(remainingSeconds % 60); + return `Approximately ${minutes}m ${seconds}s remaining`; + } + })()} + {aiValidationProgress.promptLength && ( +

+ Prompt length:{" "} + {aiValidationProgress.promptLength.toLocaleString()}{" "} + characters +

+ )} +
+ ) ); })()}
@@ -127,9 +349,11 @@ export const AiValidationDialogs: React.FC = ({
{/* AI Validation Results Dialog */} - setAiValidationDetails(prev => ({ ...prev, isOpen: open }))} + + setAiValidationDetails((prev) => ({ ...prev, isOpen: open })) + } > @@ -139,14 +363,19 @@ export const AiValidationDialogs: React.FC = ({ - {aiValidationDetails.changeDetails && aiValidationDetails.changeDetails.length > 0 ? ( + {aiValidationDetails.changeDetails && + aiValidationDetails.changeDetails.length > 0 ? (

Detailed Changes:

{aiValidationDetails.changeDetails.map((product, i) => { // Find the title change if it exists - const titleChange = product.changes.find(c => c.field === 'title'); - const titleValue = titleChange ? titleChange.corrected : product.title; - + const titleChange = product.changes.find( + (c) => c.field === "title" + ); + const titleValue = titleChange + ? titleChange.corrected + : product.title; + return (

@@ -163,29 +392,43 @@ export const AiValidationDialogs: React.FC = ({ {product.changes.map((change, j) => { - const field = fields.find(f => f.key === change.field); - 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 + const field = fields.find( + (f) => f.key === change.field ); - + 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 ( - {fieldLabel} + + {fieldLabel} + -
-
@@ -207,7 +450,10 @@ export const AiValidationDialogs: React.FC = ({ size="sm" onClick={() => { // Call the revert function directly - revertAiChange(product.productIndex, change.field); + revertAiChange( + product.productIndex, + change.field + ); }} > Revert Change @@ -226,12 +472,17 @@ export const AiValidationDialogs: React.FC = ({
) : (
- {aiValidationDetails.warnings && aiValidationDetails.warnings.length > 0 ? ( + {aiValidationDetails.warnings && + aiValidationDetails.warnings.length > 0 ? (
-

No changes were made, but the AI provided some warnings:

+

+ No changes were made, but the AI provided some warnings: +

    {aiValidationDetails.warnings.map((warning, i) => ( -
  • {warning}
  • +
  • + {warning} +
  • ))}
@@ -245,4 +496,4 @@ export const AiValidationDialogs: React.FC = ({

); -}; \ No newline at end of file +}; diff --git a/inventory/src/components/product-import/steps/ValidationStepNew/components/ValidationContainer.tsx b/inventory/src/components/product-import/steps/ValidationStepNew/components/ValidationContainer.tsx index 8749643..026c6f1 100644 --- a/inventory/src/components/product-import/steps/ValidationStepNew/components/ValidationContainer.tsx +++ b/inventory/src/components/product-import/steps/ValidationStepNew/components/ValidationContainer.tsx @@ -1200,6 +1200,7 @@ const ValidationContainer = ({ isChangeReverted={aiValidation.isChangeReverted} getFieldDisplayValueWithHighlight={aiValidation.getFieldDisplayValueWithHighlight} fields={fields} + debugData={aiValidation.currentPrompt.debugData} /> {/* Product Search Dialog */} diff --git a/inventory/src/components/product-import/steps/ValidationStepNew/hooks/useAiValidation.tsx b/inventory/src/components/product-import/steps/ValidationStepNew/hooks/useAiValidation.tsx index d63349e..bbd10d8 100644 --- a/inventory/src/components/product-import/steps/ValidationStepNew/hooks/useAiValidation.tsx +++ b/inventory/src/components/product-import/steps/ValidationStepNew/hooks/useAiValidation.tsx @@ -42,6 +42,25 @@ export interface CurrentPrompt { isOpen: boolean; prompt: string | null; 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 @@ -250,7 +269,11 @@ export const useAiValidation = ( // Function to show current prompt const showCurrentPrompt = useCallback(async () => { try { - setCurrentPrompt(prev => ({ ...prev, isLoading: true, isOpen: true })); + setCurrentPrompt(prev => ({ + ...prev, + isLoading: true, + isOpen: true + })); // Debug log the data being sent console.log('Sending products data:', { @@ -272,7 +295,7 @@ export const useAiValidation = ( }); // 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', headers: { 'Content-Type': 'application/json', @@ -294,7 +317,14 @@ export const useAiValidation = ( setCurrentPrompt(prev => ({ ...prev, 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 { throw new Error('No prompt returned from server'); diff --git a/inventory/src/components/settings/PerformanceMetrics.tsx b/inventory/src/components/settings/PerformanceMetrics.tsx index 0e3cfa9..1573aa8 100644 --- a/inventory/src/components/settings/PerformanceMetrics.tsx +++ b/inventory/src/components/settings/PerformanceMetrics.tsx @@ -133,8 +133,9 @@ export function PerformanceMetrics() { } }; - function getCategoryName(_cat_id: number): import("react").ReactNode { - throw new Error('Function not implemented.'); + function getCategoryName(cat_id: number): import("react").ReactNode { + // Simple implementation that just returns the ID as a string + return `Category ${cat_id}`; } return ( @@ -217,15 +218,19 @@ export function PerformanceMetrics() { - {abcConfigs.map((config) => ( + {abcConfigs && abcConfigs.length > 0 ? abcConfigs.map((config) => ( {config.cat_id ? getCategoryName(config.cat_id) : 'Global'} {config.vendor || 'All Vendors'} - {config.a_threshold}% - {config.b_threshold}% - {config.classification_period_days} + {config.a_threshold !== undefined ? `${config.a_threshold}%` : '0%'} + {config.b_threshold !== undefined ? `${config.b_threshold}%` : '0%'} + {config.classification_period_days || 0} - ))} + )) : ( + + No ABC configurations available + + )} - - - - {debugData && ( -
- - - Taxonomy Stats - - - {debugData.taxonomyStats ? ( -
-
Categories: {debugData.taxonomyStats.categories}
-
Themes: {debugData.taxonomyStats.themes}
-
Colors: {debugData.taxonomyStats.colors}
-
Tax Codes: {debugData.taxonomyStats.taxCodes}
-
Size Categories: {debugData.taxonomyStats.sizeCategories}
-
Suppliers: {debugData.taxonomyStats.suppliers}
-
Companies: {debugData.taxonomyStats.companies}
-
Artists: {debugData.taxonomyStats.artists}
-
- ) : ( -
No taxonomy data available
- )} -
-
- - - - Prompt Length - - -
-
-
Characters: {debugData.promptLength}
-
Tokens (est.): ~{Math.round(debugData.promptLength / 4)}
-
-
- - { - 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) - } - } - }} - /> -
- Cost: {((Math.round(debugData.promptLength / 4) / 1_000_000) * 3 * 100).toFixed(1)}¢ -
-
- {debugData.estimatedProcessingTime && ( -
-

Processing Time Estimate

- {debugData.estimatedProcessingTime.seconds ? ( -
-
- Estimated time: {formatTime(debugData.estimatedProcessingTime.seconds)} -
-
- Based on {debugData.estimatedProcessingTime.sampleCount} similar validation{debugData.estimatedProcessingTime.sampleCount !== 1 ? 's' : ''} -
-
- ) : ( -
No historical data available for this prompt size
- )} -
- )} -
-
-
- - - - Full Sample Prompt - - - - {debugData.sampleFullPrompt} - - - -
- )} - - ) -} - -// 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`; - } -} \ No newline at end of file diff --git a/inventory/src/pages/Settings.tsx b/inventory/src/pages/Settings.tsx index 35b3f46..cb5f32e 100644 --- a/inventory/src/pages/Settings.tsx +++ b/inventory/src/pages/Settings.tsx @@ -10,17 +10,52 @@ import { Alert, AlertDescription } from "@/components/ui/alert"; import { Protected } from "@/components/auth/Protected"; import { useContext, useMemo } from "react"; import { AuthContext } from "@/contexts/AuthContext"; +import { Separator } from "@/components/ui/separator"; -// Define available settings tabs with their permission requirements -const SETTINGS_TABS = [ - { id: "data-management", permission: "settings:data_management", label: "Data Management" }, - { 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: "templates", permission: "settings:templates", label: "Template Management" }, - { id: "user-management", permission: "settings:user_management", label: "User Management" } +// 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() { const { user } = useContext(AuthContext); @@ -62,131 +97,140 @@ export function Settings() { ); } + // 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 (

Settings

- - - - Data Management - - - Stock Management - - - - Performance Metrics - - - - - Calculation Settings - - - - - Template Management - - - - - User Management - - - + +
+ + {SETTINGS_GROUPS.map((group) => ( + hasAccessToGroup(group) && ( +
+

+ {group.label} +

+
+ {group.tabs.map((tab) => ( + + + {tab.label} + + + ))} +
+ {/* Only add separator if not the last group */} + {group.id !== SETTINGS_GROUPS[SETTINGS_GROUPS.length - 1].id && ( + + )} +
+ ) + ))} +
+
- - - - You don't have permission to access Data Management. - - - } - > - - - +
+ + + + You don't have permission to access Data Management. + + + } + > + + + - - - - You don't have permission to access Stock Management. - - - } - > - - - + + + + You don't have permission to access Stock Management. + + + } + > + + + - - - - You don't have permission to access Performance Metrics. - - - } - > - - - + + + + You don't have permission to access Performance Metrics. + + + } + > + + + - - - - You don't have permission to access Calculation Settings. - - - } - > - - - + + + + You don't have permission to access Calculation Settings. + + + } + > + + + - - - - You don't have permission to access Template Management. - - - } - > - - - - - - - - You don't have permission to access User Management. - - - } - > - - - + + + + You don't have permission to access Template Management. + + + } + > + + + + + + + + You don't have permission to access User Management. + + + } + > + + + +
);