From a068a253cde9bfbdfb935f0daa07568004be1138 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 27 Mar 2025 19:31:11 -0400 Subject: [PATCH] More data management page tweaks, ensure reusable images don't get deleted automatically --- inventory-server/scripts/reset-db.js | 4 +- inventory-server/scripts/reset-metrics.js | 25 +++++- inventory-server/src/routes/import.js | 51 ++++++++++-- .../src/components/dashboard/Overview.tsx | 66 +++++++++++++++ .../dashboard/VendorPerformance.tsx | 79 ++++++++++++++++++ .../components/settings/DataManagement.tsx | 80 +++---------------- inventory/src/types/products.ts | 1 + inventory/tsconfig.tsbuildinfo | 2 +- 8 files changed, 227 insertions(+), 81 deletions(-) create mode 100644 inventory/src/components/dashboard/Overview.tsx create mode 100644 inventory/src/components/dashboard/VendorPerformance.tsx diff --git a/inventory-server/scripts/reset-db.js b/inventory-server/scripts/reset-db.js index b3b52ae..3cffe15 100644 --- a/inventory-server/scripts/reset-db.js +++ b/inventory-server/scripts/reset-db.js @@ -184,7 +184,7 @@ async function resetDatabase() { SELECT string_agg(tablename, ', ') as tables FROM pg_tables WHERE schemaname = 'public' - AND tablename NOT IN ('users', 'permissions', 'user_permissions', 'calculate_history', 'import_history', 'ai_prompts', 'ai_validation_performance', 'templates'); + AND tablename NOT IN ('users', 'permissions', 'user_permissions', 'calculate_history', 'import_history', 'ai_prompts', 'ai_validation_performance', 'templates', 'reusable_images'); `); if (!tablesResult.rows[0].tables) { @@ -204,7 +204,7 @@ async function resetDatabase() { // Drop all tables except users const tables = tablesResult.rows[0].tables.split(', '); for (const table of tables) { - if (!['users'].includes(table)) { + if (!['users', 'reusable_images'].includes(table)) { await client.query(`DROP TABLE IF EXISTS "${table}" CASCADE`); } } diff --git a/inventory-server/scripts/reset-metrics.js b/inventory-server/scripts/reset-metrics.js index 03e0d28..0fc65bf 100644 --- a/inventory-server/scripts/reset-metrics.js +++ b/inventory-server/scripts/reset-metrics.js @@ -39,6 +39,19 @@ const METRICS_TABLES = [ 'vendor_details' ]; +// Tables to always protect from being dropped +const PROTECTED_TABLES = [ + 'users', + 'permissions', + 'user_permissions', + 'calculate_history', + 'import_history', + 'ai_prompts', + 'ai_validation_performance', + 'templates', + 'reusable_images' +]; + // Split SQL into individual statements function splitSQLStatements(sql) { sql = sql.replace(/\r\n/g, '\n'); @@ -109,7 +122,8 @@ async function resetMetrics() { FROM pg_tables WHERE schemaname = 'public' AND tablename = ANY($1) - `, [METRICS_TABLES]); + AND tablename NOT IN (SELECT unnest($2::text[])) + `, [METRICS_TABLES, PROTECTED_TABLES]); outputProgress({ operation: 'Initial state', @@ -126,6 +140,15 @@ async function resetMetrics() { }); for (const table of [...METRICS_TABLES].reverse()) { + // Skip protected tables + if (PROTECTED_TABLES.includes(table)) { + outputProgress({ + operation: 'Protected table', + message: `Skipping protected table: ${table}` + }); + continue; + } + try { // Use NOWAIT to avoid hanging if there's a lock await client.query(`DROP TABLE IF EXISTS "${table}" CASCADE`); diff --git a/inventory-server/src/routes/import.js b/inventory-server/src/routes/import.js index 28d2a03..74ff34b 100644 --- a/inventory-server/src/routes/import.js +++ b/inventory-server/src/routes/import.js @@ -8,7 +8,9 @@ const fs = require('fs'); // Create uploads directory if it doesn't exist const uploadsDir = path.join('/var/www/html/inventory/uploads/products'); +const reusableUploadsDir = path.join('/var/www/html/inventory/uploads/reusable'); fs.mkdirSync(uploadsDir, { recursive: true }); +fs.mkdirSync(reusableUploadsDir, { recursive: true }); // Create a Map to track image upload times and their scheduled deletion const imageUploadMap = new Map(); @@ -35,6 +37,12 @@ const connectionCache = { // Function to schedule image deletion after 24 hours const scheduleImageDeletion = (filename, filePath) => { + // Only schedule deletion for images in the products folder + if (!filePath.includes('/uploads/products/')) { + console.log(`Skipping deletion for non-product image: ${filename}`); + return; + } + // Delete any existing timeout for this file if (imageUploadMap.has(filename)) { clearTimeout(imageUploadMap.get(filename).timeoutId); @@ -407,6 +415,14 @@ router.delete('/delete-image', (req, res) => { return res.status(404).json({ error: 'File not found' }); } + // Only allow deletion of images in the products folder + if (!filePath.includes('/uploads/products/')) { + return res.status(403).json({ + error: 'Cannot delete images outside the products folder', + message: 'This image is in a protected folder and cannot be deleted through this endpoint' + }); + } + // Delete the file fs.unlinkSync(filePath); @@ -641,11 +657,19 @@ router.get('/check-file/:filename', (req, res) => { return res.status(400).json({ error: 'Invalid filename' }); } - const filePath = path.join(uploadsDir, filename); + // First check in products directory + let filePath = path.join(uploadsDir, filename); + let exists = fs.existsSync(filePath); + + // If not found in products, check in reusable directory + if (!exists) { + filePath = path.join(reusableUploadsDir, filename); + exists = fs.existsSync(filePath); + } try { // Check if file exists - if (!fs.existsSync(filePath)) { + if (!exists) { return res.status(404).json({ error: 'File not found', path: filePath, @@ -685,13 +709,23 @@ router.get('/check-file/:filename', (req, res) => { // List all files in uploads directory router.get('/list-uploads', (req, res) => { try { - if (!fs.existsSync(uploadsDir)) { - return res.status(404).json({ error: 'Uploads directory not found', path: uploadsDir }); + const { directory = 'products' } = req.query; + + // Determine which directory to list + let targetDir; + if (directory === 'reusable') { + targetDir = reusableUploadsDir; + } else { + targetDir = uploadsDir; // default to products } - const files = fs.readdirSync(uploadsDir); + if (!fs.existsSync(targetDir)) { + return res.status(404).json({ error: 'Uploads directory not found', path: targetDir }); + } + + const files = fs.readdirSync(targetDir); const fileDetails = files.map(file => { - const filePath = path.join(uploadsDir, file); + const filePath = path.join(targetDir, file); try { const stats = fs.statSync(filePath); return { @@ -709,12 +743,13 @@ router.get('/list-uploads', (req, res) => { }); return res.json({ - directory: uploadsDir, + directory: targetDir, + type: directory, count: files.length, files: fileDetails }); } catch (error) { - return res.status(500).json({ error: error.message, path: uploadsDir }); + return res.status(500).json({ error: error.message }); } }); diff --git a/inventory/src/components/dashboard/Overview.tsx b/inventory/src/components/dashboard/Overview.tsx new file mode 100644 index 0000000..cf5ea87 --- /dev/null +++ b/inventory/src/components/dashboard/Overview.tsx @@ -0,0 +1,66 @@ +import { useQuery } from '@tanstack/react-query'; +import { Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'; +import config from '../../config'; + +interface SalesData { + date: string; + total: number; +} + +export function Overview() { + const { data, isLoading, error } = useQuery({ + queryKey: ['sales-overview'], + queryFn: async () => { + const response = await fetch(`${config.apiUrl}/dashboard/sales-overview`); + if (!response.ok) { + throw new Error('Failed to fetch sales overview'); + } + const rawData = await response.json(); + return rawData.map((item: SalesData) => ({ + ...item, + total: parseFloat(item.total.toString()), + date: new Date(item.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) + })); + }, + }); + + if (isLoading) { + return
Loading chart...
; + } + + if (error) { + return
Error loading sales overview
; + } + + return ( + + + + `$${value.toLocaleString()}`} + /> + [`$${value.toLocaleString()}`, 'Sales']} + labelFormatter={(label) => `Date: ${label}`} + /> + + + + ); +} \ No newline at end of file diff --git a/inventory/src/components/dashboard/VendorPerformance.tsx b/inventory/src/components/dashboard/VendorPerformance.tsx new file mode 100644 index 0000000..52a0443 --- /dev/null +++ b/inventory/src/components/dashboard/VendorPerformance.tsx @@ -0,0 +1,79 @@ +import { useQuery } from "@tanstack/react-query" +import { CardHeader, CardTitle, CardContent } from "@/components/ui/card" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { Progress } from "@/components/ui/progress" +import config from "@/config" + +interface VendorMetrics { + vendor: string + avg_lead_time: number + on_time_delivery_rate: number + avg_fill_rate: number + total_orders: number + active_orders: number + overdue_orders: number +} + +export function VendorPerformance() { + const { data: vendors } = useQuery({ + queryKey: ["vendor-metrics"], + queryFn: async () => { + const response = await fetch(`${config.apiUrl}/dashboard/vendor/performance`) + if (!response.ok) { + throw new Error("Failed to fetch vendor metrics") + } + return response.json() + }, + }) + + // Sort vendors by on-time delivery rate + const sortedVendors = vendors + ?.sort((a, b) => b.on_time_delivery_rate - a.on_time_delivery_rate) + + return ( + <> + + Top Vendor Performance + + + + + + Vendor + On-Time + Fill Rate + + + + {sortedVendors?.map((vendor) => ( + + {vendor.vendor} + +
+ + + {vendor.on_time_delivery_rate.toFixed(0)}% + +
+
+ + {vendor.avg_fill_rate.toFixed(0)}% + +
+ ))} +
+
+
+ + ) +} \ No newline at end of file diff --git a/inventory/src/components/settings/DataManagement.tsx b/inventory/src/components/settings/DataManagement.tsx index bbc799c..5c5c5bd 100644 --- a/inventory/src/components/settings/DataManagement.tsx +++ b/inventory/src/components/settings/DataManagement.tsx @@ -29,19 +29,6 @@ import config from "../../config"; import { toast } from "sonner"; import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table"; -interface ImportProgress { - status: "running" | "error" | "complete" | "cancelled"; - operation?: string; - current?: number; - total?: number; - rate?: number; - elapsed?: string; - remaining?: string; - progress?: string; - error?: string; - percentage?: string; - message?: string; -} interface HistoryRecord { id: number; @@ -439,54 +426,9 @@ export function DataManagement() { } }; - const refreshTableStatus = async () => { - try { - const response = await fetch(`${config.apiUrl}/csv/status/tables`); - if (!response.ok) throw new Error('Failed to fetch table status'); - const data = await response.json(); - setTableStatus(Array.isArray(data) ? data : []); - toast.success("Table status refreshed"); - } catch (error) { - toast.error("Failed to refresh table status"); - setTableStatus([]); - } - }; - const refreshModuleStatus = async () => { - try { - const response = await fetch(`${config.apiUrl}/csv/status/modules`); - if (!response.ok) throw new Error('Failed to fetch module status'); - const data = await response.json(); - setModuleStatus(Array.isArray(data) ? data : []); - } catch (error) { - toast.error("Failed to refresh module status"); - setModuleStatus([]); - } - }; - const refreshImportHistory = async () => { - try { - const response = await fetch(`${config.apiUrl}/csv/history/import`); - if (!response.ok) throw new Error('Failed to fetch import history'); - const data = await response.json(); - setImportHistory(Array.isArray(data) ? data : []); - } catch (error) { - toast.error("Failed to refresh import history"); - setImportHistory([]); - } - }; - const refreshCalculateHistory = async () => { - try { - const response = await fetch(`${config.apiUrl}/csv/history/calculate`); - if (!response.ok) throw new Error('Failed to fetch calculate history'); - const data = await response.json(); - setCalculateHistory(Array.isArray(data) ? data : []); - } catch (error) { - toast.error("Failed to refresh calculate history"); - setCalculateHistory([]); - } - }; const refreshAllData = async () => { setIsLoading(true); @@ -558,10 +500,10 @@ export function DataManagement() { const renderTableCountsSection = () => { if (!tableCounts) return null; - const renderTableGroup = (title: string, tables: TableCount[]) => ( + const renderTableGroup = (_title: string, tables: TableCount[]) => (
- {tables.map((table, index) => ( + {tables.map((table) => (
{/* Full Update Card */} - - + + Full Update Import latest data and recalculate all metrics @@ -613,7 +555,7 @@ export function DataManagement() {
{isUpdating && ( - )} @@ -640,8 +582,8 @@ export function DataManagement() { {/* Full Reset Card */} - - + + Full Reset Reset database, reimport all data, and recalculate metrics @@ -653,7 +595,7 @@ export function DataManagement() { )} @@ -801,7 +743,7 @@ export function DataManagement() {
{/* Recent Import History */} - + Recent Imports diff --git a/inventory/src/types/products.ts b/inventory/src/types/products.ts index 634581f..cefb8e4 100644 --- a/inventory/src/types/products.ts +++ b/inventory/src/types/products.ts @@ -43,6 +43,7 @@ export interface Product { gross_profit?: string; // numeric(15,3) gmroi?: string; // numeric(15,3) avg_lead_time_days?: string; // numeric(15,3) + first_received_date?: string; last_received_date?: string; abc_class?: string; stock_status?: string; diff --git a/inventory/tsconfig.tsbuildinfo b/inventory/tsconfig.tsbuildinfo index 8dbe4c9..20b3941 100644 --- a/inventory/tsconfig.tsbuildinfo +++ b/inventory/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/app.tsx","./src/config.ts","./src/main.tsx","./src/vite-env.d.ts","./src/components/analytics/categoryperformance.tsx","./src/components/analytics/priceanalysis.tsx","./src/components/analytics/profitanalysis.tsx","./src/components/analytics/stockanalysis.tsx","./src/components/analytics/vendorperformance.tsx","./src/components/auth/firstaccessiblepage.tsx","./src/components/auth/protected.tsx","./src/components/auth/requireauth.tsx","./src/components/dashboard/bestsellers.tsx","./src/components/dashboard/forecastmetrics.tsx","./src/components/dashboard/overstockmetrics.tsx","./src/components/dashboard/purchasemetrics.tsx","./src/components/dashboard/replenishmentmetrics.tsx","./src/components/dashboard/salesmetrics.tsx","./src/components/dashboard/stockmetrics.tsx","./src/components/dashboard/topoverstockedproducts.tsx","./src/components/dashboard/topreplenishproducts.tsx","./src/components/forecasting/columns.tsx","./src/components/layout/appsidebar.tsx","./src/components/layout/mainlayout.tsx","./src/components/product-import/reactspreadsheetimport.tsx","./src/components/product-import/index.ts","./src/components/product-import/translationsrsiprops.ts","./src/components/product-import/types.ts","./src/components/product-import/components/modalwrapper.tsx","./src/components/product-import/components/providers.tsx","./src/components/product-import/components/table.tsx","./src/components/product-import/hooks/usersi.ts","./src/components/product-import/steps/steps.tsx","./src/components/product-import/steps/uploadflow.tsx","./src/components/product-import/steps/imageuploadstep/imageuploadstep.tsx","./src/components/product-import/steps/imageuploadstep/types.ts","./src/components/product-import/steps/imageuploadstep/components/droppablecontainer.tsx","./src/components/product-import/steps/imageuploadstep/components/genericdropzone.tsx","./src/components/product-import/steps/imageuploadstep/components/unassignedimagessection.tsx","./src/components/product-import/steps/imageuploadstep/components/productcard/copybutton.tsx","./src/components/product-import/steps/imageuploadstep/components/productcard/imagedropzone.tsx","./src/components/product-import/steps/imageuploadstep/components/productcard/productcard.tsx","./src/components/product-import/steps/imageuploadstep/components/productcard/sortableimage.tsx","./src/components/product-import/steps/imageuploadstep/components/unassignedimagessection/unassignedimageitem.tsx","./src/components/product-import/steps/imageuploadstep/hooks/usebulkimageupload.ts","./src/components/product-import/steps/imageuploadstep/hooks/usedraganddrop.ts","./src/components/product-import/steps/imageuploadstep/hooks/useproductimageoperations.ts","./src/components/product-import/steps/imageuploadstep/hooks/useproductimagesinit.ts","./src/components/product-import/steps/imageuploadstep/hooks/useurlimageupload.ts","./src/components/product-import/steps/matchcolumnsstep/matchcolumnsstep.tsx","./src/components/product-import/steps/matchcolumnsstep/components/matchicon.tsx","./src/components/product-import/steps/matchcolumnsstep/components/templatecolumn.tsx","./src/components/product-import/steps/matchcolumnsstep/utils/findmatch.ts","./src/components/product-import/steps/matchcolumnsstep/utils/findunmatchedrequiredfields.ts","./src/components/product-import/steps/matchcolumnsstep/utils/getfieldoptions.ts","./src/components/product-import/steps/matchcolumnsstep/utils/getmatchedcolumns.ts","./src/components/product-import/steps/matchcolumnsstep/utils/normalizecheckboxvalue.ts","./src/components/product-import/steps/matchcolumnsstep/utils/normalizetabledata.ts","./src/components/product-import/steps/matchcolumnsstep/utils/setcolumn.ts","./src/components/product-import/steps/matchcolumnsstep/utils/setignorecolumn.ts","./src/components/product-import/steps/matchcolumnsstep/utils/setsubcolumn.ts","./src/components/product-import/steps/matchcolumnsstep/utils/uniqueentries.ts","./src/components/product-import/steps/selectheaderstep/selectheaderstep.tsx","./src/components/product-import/steps/selectheaderstep/components/selectheadertable.tsx","./src/components/product-import/steps/selectheaderstep/components/columns.tsx","./src/components/product-import/steps/selectsheetstep/selectsheetstep.tsx","./src/components/product-import/steps/uploadstep/uploadstep.tsx","./src/components/product-import/steps/uploadstep/components/dropzone.tsx","./src/components/product-import/steps/uploadstep/components/columns.tsx","./src/components/product-import/steps/uploadstep/utils/readfilesasync.ts","./src/components/product-import/steps/validationstepnew/validationstepnew.tsx","./src/components/product-import/steps/validationstepnew/index.tsx","./src/components/product-import/steps/validationstepnew/types.ts","./src/components/product-import/steps/validationstepnew/components/aivalidationdialogs.tsx","./src/components/product-import/steps/validationstepnew/components/basecellcontent.tsx","./src/components/product-import/steps/validationstepnew/components/searchabletemplateselect.tsx","./src/components/product-import/steps/validationstepnew/components/upcvalidationtableadapter.tsx","./src/components/product-import/steps/validationstepnew/components/validationcell.tsx","./src/components/product-import/steps/validationstepnew/components/validationcontainer.tsx","./src/components/product-import/steps/validationstepnew/components/validationtable.tsx","./src/components/product-import/steps/validationstepnew/components/cells/checkboxcell.tsx","./src/components/product-import/steps/validationstepnew/components/cells/inputcell.tsx","./src/components/product-import/steps/validationstepnew/components/cells/multiselectcell.tsx","./src/components/product-import/steps/validationstepnew/components/cells/multilineinput.tsx","./src/components/product-import/steps/validationstepnew/components/cells/selectcell.tsx","./src/components/product-import/steps/validationstepnew/hooks/useaivalidation.tsx","./src/components/product-import/steps/validationstepnew/hooks/usefieldvalidation.tsx","./src/components/product-import/steps/validationstepnew/hooks/usefiltermanagement.tsx","./src/components/product-import/steps/validationstepnew/hooks/useproductlinesfetching.tsx","./src/components/product-import/steps/validationstepnew/hooks/userowoperations.tsx","./src/components/product-import/steps/validationstepnew/hooks/usetemplatemanagement.tsx","./src/components/product-import/steps/validationstepnew/hooks/useuniqueitemnumbersvalidation.tsx","./src/components/product-import/steps/validationstepnew/hooks/useuniquevalidation.tsx","./src/components/product-import/steps/validationstepnew/hooks/useupcvalidation.tsx","./src/components/product-import/steps/validationstepnew/hooks/usevalidation.tsx","./src/components/product-import/steps/validationstepnew/hooks/usevalidationstate.tsx","./src/components/product-import/steps/validationstepnew/hooks/validationtypes.ts","./src/components/product-import/steps/validationstepnew/types/index.ts","./src/components/product-import/steps/validationstepnew/utils/datamutations.ts","./src/components/product-import/utils/exceedsmaxrecords.ts","./src/components/product-import/utils/mapdata.ts","./src/components/product-import/utils/mapworkbook.ts","./src/components/product-import/utils/steps.ts","./src/components/products/productdetail.tsx","./src/components/products/productfilters.tsx","./src/components/products/producttable.tsx","./src/components/products/producttableskeleton.tsx","./src/components/products/productviews.tsx","./src/components/settings/calculationsettings.tsx","./src/components/settings/configuration.tsx","./src/components/settings/datamanagement.tsx","./src/components/settings/performancemetrics.tsx","./src/components/settings/permissionselector.tsx","./src/components/settings/promptmanagement.tsx","./src/components/settings/reusableimagemanagement.tsx","./src/components/settings/stockmanagement.tsx","./src/components/settings/templatemanagement.tsx","./src/components/settings/userform.tsx","./src/components/settings/userlist.tsx","./src/components/settings/usermanagement.tsx","./src/components/templates/searchproducttemplatedialog.tsx","./src/components/templates/templateform.tsx","./src/components/ui/accordion.tsx","./src/components/ui/alert-dialog.tsx","./src/components/ui/alert.tsx","./src/components/ui/avatar.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/calendar.tsx","./src/components/ui/card.tsx","./src/components/ui/checkbox.tsx","./src/components/ui/code.tsx","./src/components/ui/command.tsx","./src/components/ui/date-range-picker-narrow.tsx","./src/components/ui/date-range-picker.tsx","./src/components/ui/dialog.tsx","./src/components/ui/drawer.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/form.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/pagination.tsx","./src/components/ui/popover.tsx","./src/components/ui/progress.tsx","./src/components/ui/radio-group.tsx","./src/components/ui/scroll-area.tsx","./src/components/ui/select.tsx","./src/components/ui/separator.tsx","./src/components/ui/sheet.tsx","./src/components/ui/sidebar.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/sonner.tsx","./src/components/ui/switch.tsx","./src/components/ui/table.tsx","./src/components/ui/tabs.tsx","./src/components/ui/textarea.tsx","./src/components/ui/toast.tsx","./src/components/ui/toaster.tsx","./src/components/ui/toggle-group.tsx","./src/components/ui/toggle.tsx","./src/components/ui/tooltip.tsx","./src/contexts/authcontext.tsx","./src/hooks/use-mobile.tsx","./src/hooks/use-toast.ts","./src/lib/utils.ts","./src/pages/analytics.tsx","./src/pages/categories.tsx","./src/pages/dashboard.tsx","./src/pages/forecasting.tsx","./src/pages/import.tsx","./src/pages/login.tsx","./src/pages/products.tsx","./src/pages/purchaseorders.tsx","./src/pages/settings.tsx","./src/pages/vendors.tsx","./src/types/globals.d.ts","./src/types/products.ts","./src/types/react-data-grid.d.ts","./src/types/status-codes.ts"],"version":"5.6.3"} \ No newline at end of file +{"root":["./src/app.tsx","./src/config.ts","./src/main.tsx","./src/vite-env.d.ts","./src/components/analytics/categoryperformance.tsx","./src/components/analytics/priceanalysis.tsx","./src/components/analytics/profitanalysis.tsx","./src/components/analytics/stockanalysis.tsx","./src/components/analytics/vendorperformance.tsx","./src/components/auth/firstaccessiblepage.tsx","./src/components/auth/protected.tsx","./src/components/auth/requireauth.tsx","./src/components/dashboard/bestsellers.tsx","./src/components/dashboard/forecastmetrics.tsx","./src/components/dashboard/overstockmetrics.tsx","./src/components/dashboard/overview.tsx","./src/components/dashboard/purchasemetrics.tsx","./src/components/dashboard/replenishmentmetrics.tsx","./src/components/dashboard/salesmetrics.tsx","./src/components/dashboard/stockmetrics.tsx","./src/components/dashboard/topoverstockedproducts.tsx","./src/components/dashboard/topreplenishproducts.tsx","./src/components/dashboard/vendorperformance.tsx","./src/components/forecasting/columns.tsx","./src/components/layout/appsidebar.tsx","./src/components/layout/mainlayout.tsx","./src/components/product-import/reactspreadsheetimport.tsx","./src/components/product-import/index.ts","./src/components/product-import/translationsrsiprops.ts","./src/components/product-import/types.ts","./src/components/product-import/components/modalwrapper.tsx","./src/components/product-import/components/providers.tsx","./src/components/product-import/components/table.tsx","./src/components/product-import/hooks/usersi.ts","./src/components/product-import/steps/steps.tsx","./src/components/product-import/steps/uploadflow.tsx","./src/components/product-import/steps/imageuploadstep/imageuploadstep.tsx","./src/components/product-import/steps/imageuploadstep/types.ts","./src/components/product-import/steps/imageuploadstep/components/droppablecontainer.tsx","./src/components/product-import/steps/imageuploadstep/components/genericdropzone.tsx","./src/components/product-import/steps/imageuploadstep/components/unassignedimagessection.tsx","./src/components/product-import/steps/imageuploadstep/components/productcard/copybutton.tsx","./src/components/product-import/steps/imageuploadstep/components/productcard/imagedropzone.tsx","./src/components/product-import/steps/imageuploadstep/components/productcard/productcard.tsx","./src/components/product-import/steps/imageuploadstep/components/productcard/sortableimage.tsx","./src/components/product-import/steps/imageuploadstep/components/unassignedimagessection/unassignedimageitem.tsx","./src/components/product-import/steps/imageuploadstep/hooks/usebulkimageupload.ts","./src/components/product-import/steps/imageuploadstep/hooks/usedraganddrop.ts","./src/components/product-import/steps/imageuploadstep/hooks/useproductimageoperations.ts","./src/components/product-import/steps/imageuploadstep/hooks/useproductimagesinit.ts","./src/components/product-import/steps/imageuploadstep/hooks/useurlimageupload.ts","./src/components/product-import/steps/matchcolumnsstep/matchcolumnsstep.tsx","./src/components/product-import/steps/matchcolumnsstep/components/matchicon.tsx","./src/components/product-import/steps/matchcolumnsstep/components/templatecolumn.tsx","./src/components/product-import/steps/matchcolumnsstep/utils/findmatch.ts","./src/components/product-import/steps/matchcolumnsstep/utils/findunmatchedrequiredfields.ts","./src/components/product-import/steps/matchcolumnsstep/utils/getfieldoptions.ts","./src/components/product-import/steps/matchcolumnsstep/utils/getmatchedcolumns.ts","./src/components/product-import/steps/matchcolumnsstep/utils/normalizecheckboxvalue.ts","./src/components/product-import/steps/matchcolumnsstep/utils/normalizetabledata.ts","./src/components/product-import/steps/matchcolumnsstep/utils/setcolumn.ts","./src/components/product-import/steps/matchcolumnsstep/utils/setignorecolumn.ts","./src/components/product-import/steps/matchcolumnsstep/utils/setsubcolumn.ts","./src/components/product-import/steps/matchcolumnsstep/utils/uniqueentries.ts","./src/components/product-import/steps/selectheaderstep/selectheaderstep.tsx","./src/components/product-import/steps/selectheaderstep/components/selectheadertable.tsx","./src/components/product-import/steps/selectheaderstep/components/columns.tsx","./src/components/product-import/steps/selectsheetstep/selectsheetstep.tsx","./src/components/product-import/steps/uploadstep/uploadstep.tsx","./src/components/product-import/steps/uploadstep/components/dropzone.tsx","./src/components/product-import/steps/uploadstep/components/columns.tsx","./src/components/product-import/steps/uploadstep/utils/readfilesasync.ts","./src/components/product-import/steps/validationstepnew/validationstepnew.tsx","./src/components/product-import/steps/validationstepnew/index.tsx","./src/components/product-import/steps/validationstepnew/types.ts","./src/components/product-import/steps/validationstepnew/components/aivalidationdialogs.tsx","./src/components/product-import/steps/validationstepnew/components/basecellcontent.tsx","./src/components/product-import/steps/validationstepnew/components/searchabletemplateselect.tsx","./src/components/product-import/steps/validationstepnew/components/upcvalidationtableadapter.tsx","./src/components/product-import/steps/validationstepnew/components/validationcell.tsx","./src/components/product-import/steps/validationstepnew/components/validationcontainer.tsx","./src/components/product-import/steps/validationstepnew/components/validationtable.tsx","./src/components/product-import/steps/validationstepnew/components/cells/checkboxcell.tsx","./src/components/product-import/steps/validationstepnew/components/cells/inputcell.tsx","./src/components/product-import/steps/validationstepnew/components/cells/multiselectcell.tsx","./src/components/product-import/steps/validationstepnew/components/cells/multilineinput.tsx","./src/components/product-import/steps/validationstepnew/components/cells/selectcell.tsx","./src/components/product-import/steps/validationstepnew/hooks/useaivalidation.tsx","./src/components/product-import/steps/validationstepnew/hooks/usefieldvalidation.tsx","./src/components/product-import/steps/validationstepnew/hooks/usefiltermanagement.tsx","./src/components/product-import/steps/validationstepnew/hooks/useproductlinesfetching.tsx","./src/components/product-import/steps/validationstepnew/hooks/userowoperations.tsx","./src/components/product-import/steps/validationstepnew/hooks/usetemplatemanagement.tsx","./src/components/product-import/steps/validationstepnew/hooks/useuniqueitemnumbersvalidation.tsx","./src/components/product-import/steps/validationstepnew/hooks/useuniquevalidation.tsx","./src/components/product-import/steps/validationstepnew/hooks/useupcvalidation.tsx","./src/components/product-import/steps/validationstepnew/hooks/usevalidation.tsx","./src/components/product-import/steps/validationstepnew/hooks/usevalidationstate.tsx","./src/components/product-import/steps/validationstepnew/hooks/validationtypes.ts","./src/components/product-import/steps/validationstepnew/types/index.ts","./src/components/product-import/steps/validationstepnew/utils/datamutations.ts","./src/components/product-import/utils/exceedsmaxrecords.ts","./src/components/product-import/utils/mapdata.ts","./src/components/product-import/utils/mapworkbook.ts","./src/components/product-import/utils/steps.ts","./src/components/products/productdetail.tsx","./src/components/products/productfilters.tsx","./src/components/products/producttable.tsx","./src/components/products/producttableskeleton.tsx","./src/components/products/productviews.tsx","./src/components/settings/calculationsettings.tsx","./src/components/settings/configuration.tsx","./src/components/settings/datamanagement.tsx","./src/components/settings/performancemetrics.tsx","./src/components/settings/permissionselector.tsx","./src/components/settings/promptmanagement.tsx","./src/components/settings/reusableimagemanagement.tsx","./src/components/settings/stockmanagement.tsx","./src/components/settings/templatemanagement.tsx","./src/components/settings/userform.tsx","./src/components/settings/userlist.tsx","./src/components/settings/usermanagement.tsx","./src/components/templates/searchproducttemplatedialog.tsx","./src/components/templates/templateform.tsx","./src/components/ui/accordion.tsx","./src/components/ui/alert-dialog.tsx","./src/components/ui/alert.tsx","./src/components/ui/avatar.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/calendar.tsx","./src/components/ui/card.tsx","./src/components/ui/checkbox.tsx","./src/components/ui/code.tsx","./src/components/ui/command.tsx","./src/components/ui/date-range-picker-narrow.tsx","./src/components/ui/date-range-picker.tsx","./src/components/ui/dialog.tsx","./src/components/ui/drawer.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/form.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/pagination.tsx","./src/components/ui/popover.tsx","./src/components/ui/progress.tsx","./src/components/ui/radio-group.tsx","./src/components/ui/scroll-area.tsx","./src/components/ui/select.tsx","./src/components/ui/separator.tsx","./src/components/ui/sheet.tsx","./src/components/ui/sidebar.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/sonner.tsx","./src/components/ui/switch.tsx","./src/components/ui/table.tsx","./src/components/ui/tabs.tsx","./src/components/ui/textarea.tsx","./src/components/ui/toast.tsx","./src/components/ui/toaster.tsx","./src/components/ui/toggle-group.tsx","./src/components/ui/toggle.tsx","./src/components/ui/tooltip.tsx","./src/contexts/authcontext.tsx","./src/hooks/use-mobile.tsx","./src/hooks/use-toast.ts","./src/lib/utils.ts","./src/pages/analytics.tsx","./src/pages/categories.tsx","./src/pages/dashboard.tsx","./src/pages/forecasting.tsx","./src/pages/import.tsx","./src/pages/login.tsx","./src/pages/products.tsx","./src/pages/purchaseorders.tsx","./src/pages/settings.tsx","./src/pages/vendors.tsx","./src/types/globals.d.ts","./src/types/products.ts","./src/types/react-data-grid.d.ts","./src/types/status-codes.ts"],"version":"5.6.3"} \ No newline at end of file