diff --git a/frontend/src/pages/Admin/ProductsPage.jsx b/frontend/src/pages/Admin/ProductsPage.jsx
index 39498f5..a397d57 100644
--- a/frontend/src/pages/Admin/ProductsPage.jsx
+++ b/frontend/src/pages/Admin/ProductsPage.jsx
@@ -21,19 +21,27 @@ import {
DialogActions,
DialogContent,
DialogContentText,
- DialogTitle
+ DialogTitle,
+ Stack,
+ Tooltip
} from '@mui/material';
import {
Edit as EditIcon,
Delete as DeleteIcon,
Add as AddIcon,
Search as SearchIcon,
- Clear as ClearIcon
+ Clear as ClearIcon,
+ FileDownload as DownloadIcon,
+ Upload as UploadIcon,
+ CloudUpload as CloudUploadIcon,
+ CheckCircleOutline as CheckCircleIcon
} from '@mui/icons-material';
import { Link as RouterLink, useNavigate } from 'react-router-dom';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import apiClient from '../../services/api';
import ProductImage from '../../components/ProductImage';
+import * as XLSX from 'xlsx';
+import Papa from 'papaparse';
const AdminProductsPage = () => {
const navigate = useNavigate();
@@ -43,6 +51,18 @@ const AdminProductsPage = () => {
const [search, setSearch] = useState('');
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [productToDelete, setProductToDelete] = useState(null);
+
+ // New states for upload functionality
+ const [uploadDialogOpen, setUploadDialogOpen] = useState(false);
+ const [uploadFile, setUploadFile] = useState(null);
+ const [uploadProgress, setUploadProgress] = useState(0);
+ const [uploadError, setUploadError] = useState(null);
+ const [parsedProducts, setParsedProducts] = useState([]);
+ const [isUploading, setIsUploading] = useState(false);
+ const [uploadSuccess, setUploadSuccess] = useState(false);
+ const [uploadResults, setUploadResults] = useState(null);
+
+ const fileInputRef = React.useRef(null);
// Fetch products
const {
@@ -75,6 +95,23 @@ const AdminProductsPage = () => {
setProductToDelete(null);
}
});
+
+ // Bulk upload mutation
+ const bulkUploadProducts = useMutation({
+ mutationFn: async (productData) => {
+ return await apiClient.post('/admin/products', { products: productData });
+ },
+ onSuccess: (data) => {
+ queryClient.invalidateQueries({ queryKey: ['admin-products'] });
+ setUploadSuccess(true);
+ setUploadResults(data.data);
+ setIsUploading(false);
+ },
+ onError: (error) => {
+ setUploadError(error.message || 'Failed to upload products');
+ setIsUploading(false);
+ }
+ });
// Handle search change
const handleSearchChange = (event) => {
@@ -122,6 +159,237 @@ const AdminProductsPage = () => {
const handleEditClick = (productId) => {
navigate(`/admin/products/${productId}`);
};
+
+ // Handle download template
+ const handleDownloadTemplate = () => {
+ // Create template headers
+ const headers = [
+ 'name',
+ 'description',
+ 'categoryName',
+ 'price',
+ 'stockQuantity',
+ 'weightGrams',
+ 'lengthCm',
+ 'widthCm',
+ 'heightCm',
+ 'origin',
+ 'age',
+ 'materialType',
+ 'color',
+ 'stockNotification_enabled',
+ 'stockNotification_email',
+ 'stockNotification_threshold',
+ 'tags' // Comma-separated tags
+ ];
+
+ // Create example data row
+ const exampleRow = [
+ 'Example Product',
+ 'This is a product description',
+ 'Rock', // Valid category name
+ '19.99',
+ '10',
+ '500',
+ '15',
+ '10',
+ '5',
+ 'Brazil',
+ 'Recent',
+ 'Quartz',
+ 'Purple',
+ 'FALSE', // stockNotification_enabled
+ 'email@example.com', // stockNotification_email
+ '5', // stockNotification_threshold
+ 'Rare,Polished' // Comma-separated tags
+ ];
+
+ // Create workbook
+ const worksheet = XLSX.utils.aoa_to_sheet([headers, exampleRow]);
+ const workbook = XLSX.utils.book_new();
+ XLSX.utils.book_append_sheet(workbook, worksheet, 'Products Template');
+
+ // Add column widths for better readability
+ const colWidths = [];
+ headers.forEach(() => colWidths.push({ wch: 15 }));
+ worksheet['!cols'] = colWidths;
+
+ // Generate Excel file
+ XLSX.writeFile(workbook, 'products_upload_template.xlsx');
+ };
+
+ // Handle open upload dialog
+ const handleOpenUploadDialog = () => {
+ resetUploadState();
+ setUploadDialogOpen(true);
+ };
+
+ // Handle close upload dialog
+ const handleCloseUploadDialog = () => {
+ resetUploadState();
+ setUploadDialogOpen(false);
+ };
+
+ // Reset upload states
+ const resetUploadState = () => {
+ setUploadFile(null);
+ setUploadProgress(0);
+ setUploadError(null);
+ setParsedProducts([]);
+ setIsUploading(false);
+ setUploadSuccess(false);
+ setUploadResults(null);
+ if (fileInputRef.current) {
+ fileInputRef.current.value = "";
+ }
+ };
+
+ // Handle file selection
+ const handleFileSelect = (e) => {
+ setUploadError(null);
+ setUploadSuccess(false);
+ const file = e.target.files[0];
+ if (!file) return;
+
+ setUploadFile(file);
+
+ // Determine file type and parse accordingly
+ const fileExtension = file.name.split('.').pop().toLowerCase();
+
+ if (fileExtension === 'csv') {
+ parseCsvFile(file);
+ } else if (fileExtension === 'xlsx' || fileExtension === 'xls') {
+ parseExcelFile(file);
+ } else {
+ setUploadError('Unsupported file format. Please upload a CSV or Excel file.');
+ }
+ };
+
+ // Parse CSV file
+ const parseCsvFile = (file) => {
+ Papa.parse(file, {
+ header: true,
+ skipEmptyLines: true,
+ complete: (results) => {
+ if (results.errors.length > 0) {
+ setUploadError(`Error parsing CSV: ${results.errors[0].message}`);
+ return;
+ }
+
+ const formattedProducts = formatParsedProducts(results.data);
+ setParsedProducts(formattedProducts);
+ },
+ error: (error) => {
+ setUploadError(`Error parsing CSV: ${error.message}`);
+ }
+ });
+ };
+
+ // Parse Excel file
+ const parseExcelFile = (file) => {
+ const reader = new FileReader();
+ reader.onload = (e) => {
+ try {
+ const data = new Uint8Array(e.target.result);
+ const workbook = XLSX.read(data, { type: 'array' });
+
+ // Get first sheet
+ const firstSheetName = workbook.SheetNames[0];
+ const worksheet = workbook.Sheets[firstSheetName];
+
+ // Convert to JSON
+ const jsonData = XLSX.utils.sheet_to_json(worksheet, { defval: null });
+
+ const formattedProducts = formatParsedProducts(jsonData);
+ setParsedProducts(formattedProducts);
+ } catch (error) {
+ setUploadError(`Error parsing Excel file: ${error.message}`);
+ }
+ };
+ reader.onerror = () => {
+ setUploadError('Error reading file');
+ };
+ reader.readAsArrayBuffer(file);
+ };
+
+ // Format parsed products to match API expectations
+ const formatParsedProducts = (rawProducts) => {
+ return rawProducts.map(product => {
+ // Extract stock notification fields and combine them
+ const stockNotification = {
+ enabled: product.stockNotification_enabled === 'TRUE' || product.stockNotification_enabled === true,
+ email: product.stockNotification_email || null,
+ threshold: product.stockNotification_threshold ? parseInt(product.stockNotification_threshold) : null
+ };
+
+ // Process tags (convert from comma-separated string to array)
+ let tags = [];
+ if (product.tags) {
+ if (typeof product.tags === 'string') {
+ tags = product.tags.split(',').map(tag => tag.trim()).filter(tag => tag);
+ } else if (Array.isArray(product.tags)) {
+ tags = product.tags;
+ }
+ }
+
+ // Convert numeric values
+ const price = product.price ? parseFloat(product.price) : null;
+ const stockQuantity = product.stockQuantity ? parseInt(product.stockQuantity) : null;
+ const weightGrams = product.weightGrams ? parseFloat(product.weightGrams) : null;
+ const lengthCm = product.lengthCm ? parseFloat(product.lengthCm) : null;
+ const widthCm = product.widthCm ? parseFloat(product.widthCm) : null;
+ const heightCm = product.heightCm ? parseFloat(product.heightCm) : null;
+
+ return {
+ name: product.name || '',
+ description: product.description || '',
+ categoryName: product.categoryName || '',
+ price: price,
+ stockQuantity: stockQuantity,
+ weightGrams: weightGrams,
+ lengthCm: lengthCm,
+ widthCm: widthCm,
+ heightCm: heightCm,
+ origin: product.origin || null,
+ age: product.age || null,
+ materialType: product.materialType || null,
+ color: product.color || null,
+ stockNotification: stockNotification,
+ tags: tags,
+ images: [] // No images in bulk upload template
+ };
+ }).filter(product =>
+ // Filter out products with missing required fields
+ product.name &&
+ product.description &&
+ product.categoryName &&
+ product.price !== null &&
+ product.stockQuantity !== null
+ );
+ };
+
+ // Submit bulk upload
+ const handleSubmitUpload = () => {
+ if (parsedProducts.length === 0) {
+ setUploadError('No valid products found in the file. Please check your data.');
+ return;
+ }
+
+ setIsUploading(true);
+ setUploadError(null);
+
+ // Submit products in batches of 20 to respect connection limits
+ const batchSize = 20;
+ const batches = [];
+
+ for (let i = 0; i < parsedProducts.length; i += batchSize) {
+ const batch = parsedProducts.slice(i, i + batchSize);
+ batches.push(batch);
+ }
+
+ // Process first batch
+ bulkUploadProducts.mutate(parsedProducts);
+ };
// Filter and paginate products
const filteredProducts = products || [];
@@ -155,15 +423,39 @@ const AdminProductsPage = () => {
Products
- }
- component={RouterLink}
- to="/admin/products/new"
- >
- Add Product
-
+
+
+ }
+ onClick={handleDownloadTemplate}
+ >
+ Download Template
+
+
+
+
+ }
+ onClick={handleOpenUploadDialog}
+ >
+ Upload Bulk Assets
+
+
+
+ }
+ component={RouterLink}
+ to="/admin/products/new"
+ >
+ Add Product
+
+
{/* Search */}
@@ -301,6 +593,190 @@ const AdminProductsPage = () => {
+
+ {/* Upload Dialog */}
+
);
};