440 lines
14 KiB
JavaScript
440 lines
14 KiB
JavaScript
import React, { useEffect, useState, useCallback, useRef } from 'react';
|
|
import {
|
|
Container,
|
|
List,
|
|
ListItem,
|
|
ListItemText,
|
|
TextField,
|
|
Button,
|
|
IconButton,
|
|
Typography,
|
|
Avatar,
|
|
ListItemAvatar,
|
|
Dialog,
|
|
DialogTitle,
|
|
DialogContent,
|
|
DialogActions,
|
|
TableContainer,
|
|
Table,
|
|
TableHead,
|
|
TableRow,
|
|
TableCell,
|
|
TableBody,
|
|
Box,
|
|
Tooltip
|
|
} from '@mui/material';
|
|
|
|
import { Delete as DeleteIcon, Edit as EditIcon } from '@mui/icons-material';
|
|
import axios from 'axios';
|
|
import { useParams, useLocation } from 'react-router-dom';
|
|
import ItemDetails from './ItemDetails';
|
|
import { PRIMARY_COLOR, SECONDARY_COLOR } from '../App';
|
|
|
|
export default function Items({ token }) {
|
|
const { id: boxId } = useParams();
|
|
const [items, setItems] = useState([]);
|
|
const [newItemName, setNewItemName] = useState('');
|
|
const [newItemDescription, setNewItemDescription] = useState('');
|
|
// const [newItemImagePath, setNewItemImagePath] = useState('/images/default.jpg');
|
|
const [editingItem, setEditingItem] = useState(null);
|
|
const location = useLocation();
|
|
const boxName = location.state?.boxName || 'Unknown Box';
|
|
// const boxID = location.state?.boxId; // used in handleClose function
|
|
const [itemImages, setItemImages] = useState({});
|
|
const fileInputRef = useRef(null);
|
|
const [openAddItemDialog, setOpenAddItemDialog] = useState(false); // For Add Item dialog
|
|
const { id } = useParams();
|
|
const boxID = id;
|
|
const url = boxId === undefined ? `${process.env.REACT_APP_API_URL}/api/v1/items` : `${process.env.REACT_APP_API_URL}/api/v1/boxes/${boxId}/items`;
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
|
|
const debugLog = (message) => {
|
|
if (process.env.DEBUG_API) {
|
|
console.log(message);
|
|
}
|
|
};
|
|
debugLog("Box ID: " + boxID);
|
|
|
|
// const handleSelectItem = (item) => {
|
|
// setSelectedItem(item);
|
|
// };
|
|
const handleAddItem = () => {
|
|
setOpenAddItemDialog(true);
|
|
};
|
|
|
|
const handleCloseAddItemDialog = () => {
|
|
setOpenAddItemDialog(false);
|
|
setNewItemName('');
|
|
setNewItemDescription('');
|
|
// setNewItemImagePath('');
|
|
if (fileInputRef.current) {
|
|
fileInputRef.current.value = '';
|
|
}
|
|
};
|
|
|
|
/**
|
|
* This function takes an image name and returns a unique image name.
|
|
* The purpose of this function is to prevent overwriting of images with the same name.
|
|
* If the image name is 'image.jpg', a random string is generated and appended to the image name.
|
|
* This is used to prevent overwriting of images with the same name.
|
|
* For example, if an image named 'image.jpg' is uploaded, the function will return 'image_8xgu6hcu.jpg'
|
|
* This ensures that the image will not overwrite any existing image with the same name.
|
|
* @param {string} imageName - The name of the image
|
|
* @return {string} - The unique image name
|
|
*/
|
|
const generateUniqueImageName = (imageName) => {
|
|
if (imageName.toLowerCase() === 'image.jpg') {
|
|
// Generate a random string
|
|
const randomString = Math.random().toString(36).substr(2, 9);
|
|
// Append the random string to the image name
|
|
return `image_${randomString}.jpg`;
|
|
}
|
|
// Return the original image name if it's not 'image.jpg'
|
|
return imageName;
|
|
};
|
|
|
|
|
|
const handleImageUpload = async (itemId, imageFile, newImageName) => {
|
|
const formData = new FormData();
|
|
//const imageFile = fileInputRef.current.files[0];
|
|
//const newImageName = generateUniqueImageName(imageFile.name);
|
|
formData.append('image', new File([imageFile], newImageName, {
|
|
type: imageFile.type,
|
|
}));
|
|
|
|
// Create a new file with the unique name
|
|
// eslint-disable-next-line
|
|
const newImageFile = new File([imageFile], newImageName, {
|
|
type: imageFile.type,
|
|
});
|
|
|
|
try {
|
|
const response = await axios.post(`${process.env.REACT_APP_API_URL}/api/v1/items/${itemId}/upload`, formData, {
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
'Content-Type': 'multipart/form-data'
|
|
}
|
|
});
|
|
// console.log('Image uploaded successfully!');
|
|
return response.data.imagePath; // Indicate successful upload
|
|
} catch (error) {
|
|
console.error('Image upload failed:', error);
|
|
return null; // Indicate upload failure
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Handle saving a new item.
|
|
*
|
|
* This function first creates the item without the image, then if the item creation is successful,
|
|
* it uploads the image associated with the item.
|
|
*
|
|
* @return {Promise<void>}
|
|
*/
|
|
const handleSaveNewItem = async () => {
|
|
try {
|
|
// Step 1: Create the item first
|
|
// This sends a request to the API to create a new item with the
|
|
// name and description provided. The box_id is set to the selected
|
|
// box ID.
|
|
const newItemResponse = await axios.post(`${process.env.REACT_APP_API_URL}/api/v1/items`, {
|
|
name: newItemName,
|
|
description: newItemDescription,
|
|
box_id: parseInt(boxId, 10) // Ensure boxId is converted to an integer
|
|
}, {
|
|
headers: {
|
|
Authorization: `Bearer ${token}`
|
|
}
|
|
});
|
|
|
|
console.log('New item created:', newItemResponse.status);
|
|
|
|
// Step 2: If item creation is successful, upload the image
|
|
if (newItemResponse.status === 200 && fileInputRef.current.files[0]) {
|
|
// Get the ID of the newly created item
|
|
const newItemId = newItemResponse.data.id;
|
|
|
|
// Get the image file that was uploaded
|
|
const imageFile = fileInputRef.current.files[0];
|
|
|
|
// Generate a unique image name by appending a random string
|
|
// to the image file name. This is used to prevent overwriting
|
|
// of images with the same name.
|
|
const newImageName = generateUniqueImageName(imageFile.name);
|
|
|
|
// Upload the image file with the unique name to the server
|
|
const uploadedImagePath = await handleImageUpload(newItemId, fileInputRef.current.files[0], newImageName);
|
|
|
|
if (uploadedImagePath) {
|
|
// The image was uploaded successfully. Log the uploaded image path
|
|
console.log("Image path to save:", uploadedImagePath);
|
|
|
|
// You might want to update your item in the backend with the image path
|
|
// For example:
|
|
// await axios.put(...);
|
|
|
|
} else {
|
|
// The image upload failed. Log an error message
|
|
console.error('Failed to upload image for the new item.');
|
|
}
|
|
}
|
|
|
|
// Close the add item dialog
|
|
handleCloseAddItemDialog();
|
|
|
|
// Fetch the items again to get the latest data
|
|
fetchItems();
|
|
} catch (error) {
|
|
// Catch any errors that may occur during the item creation
|
|
// and image upload process. Log the error message
|
|
console.error('Error adding item:', error);
|
|
}
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
//const [selectedItem, setSelectedItem] = React.useState(null);
|
|
|
|
const handleCloseItemDetails = () => {
|
|
setEditingItem(null); // Close the ItemDetails modal
|
|
};
|
|
|
|
const handleImageError = (e) => {
|
|
if (e.target.src.startsWith('data:image/')) {
|
|
console.error("Default image failed to load. Check the file path.");
|
|
return;
|
|
}
|
|
const reader = new FileReader();
|
|
reader.onload = () => {
|
|
e.target.onerror = null;
|
|
e.target.src = reader.result;
|
|
};
|
|
fetch('/default.jpg')
|
|
.then(res => res.blob())
|
|
.then(blob => reader.readAsDataURL(blob))
|
|
.catch(error => console.error("Error loading default image:", error));
|
|
};
|
|
|
|
const getImageSrc = useCallback((itemId) => {
|
|
return axios.get(`${process.env.REACT_APP_API_URL}/api/v1/items/${itemId}/image`, {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
responseType: 'blob'
|
|
})
|
|
.then(response => {
|
|
if (response.status === 200) {
|
|
return new Promise((resolve, reject) => {
|
|
const reader = new FileReader();
|
|
reader.onload = () => resolve(reader.result);
|
|
reader.onerror = reject;
|
|
reader.readAsDataURL(response.data);
|
|
});
|
|
} else {
|
|
throw new Error('Image fetch failed');
|
|
}
|
|
})
|
|
.catch(() => {
|
|
return new Promise((resolve, reject) => {
|
|
const img = new Image();
|
|
img.src = '/default.jpg';
|
|
img.onload = () => {
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = img.width;
|
|
canvas.height = img.height;
|
|
const ctx = canvas.getContext('2d');
|
|
ctx.drawImage(img, 0, 0);
|
|
resolve(canvas.toDataURL());
|
|
};
|
|
img.onerror = reject;
|
|
});
|
|
});
|
|
}, [token]);
|
|
|
|
const fetchItems = useCallback(() => {
|
|
axios.get( url, {
|
|
headers: { Authorization: `Bearer ${token}` }
|
|
}).then(response => {
|
|
setItems(response.data);
|
|
|
|
// Fetch images for each item
|
|
response.data.forEach(item => {
|
|
getImageSrc(item.ID).then(imageDataUrl => {
|
|
setItemImages(prevItemImages => ({
|
|
...prevItemImages,
|
|
[item.ID]: imageDataUrl
|
|
}));
|
|
});
|
|
});
|
|
});
|
|
}, [token, getImageSrc, url]);
|
|
// lint says I don't need boxId here
|
|
|
|
useEffect(() => {
|
|
fetchItems();
|
|
}, [boxId, token, fetchItems]);
|
|
|
|
// const handleAddItem = () => {
|
|
// const formData = new FormData();
|
|
// formData.append('name', newItemName);
|
|
// formData.append('description', newItemDescription);
|
|
// formData.append('box_id', parseInt(boxId, 10));
|
|
// // Append image only if a new one is selected
|
|
// if (fileInputRef.current.files[0]) {
|
|
// formData.append('image', fileInputRef.current.files[0]);
|
|
// }
|
|
|
|
// axios.post(`${process.env.REACT_APP_API_URL}/items`, formData, {
|
|
// headers: {
|
|
// Authorization: `Bearer ${token}`,
|
|
// 'Content-Type': 'multipart/form-data' // Important for file uploads
|
|
// }
|
|
// }).then(() => {
|
|
// setNewItemName('');
|
|
// setNewItemDescription('');
|
|
// setNewItemImagePath('');
|
|
// // Clear the file input
|
|
// if (fileInputRef.current) {
|
|
// fileInputRef.current.value = '';
|
|
// }
|
|
// fetchItems();
|
|
// });
|
|
// };
|
|
|
|
const handleDeleteItem = (itemId) => {
|
|
axios.delete(`${process.env.REACT_APP_API_URL}/api/v1/items/${itemId}`, {
|
|
headers: { Authorization: `Bearer ${token}` }
|
|
}).then(() => {
|
|
fetchItems();
|
|
});
|
|
};
|
|
|
|
const handleEditItem = (item) => {
|
|
setEditingItem(item);
|
|
};
|
|
|
|
const handleSaveEdit = () => {
|
|
setEditingItem(null);
|
|
fetchItems();
|
|
};
|
|
|
|
return (
|
|
<Container>
|
|
<TextField
|
|
label="Search"
|
|
variant="outlined"
|
|
fullWidth
|
|
margin="normal"
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
/>
|
|
<h2>Items in Box: {boxName === "Unknown Box" ? "All Boxes" : `${boxName} (${items.length} items)`}</h2>
|
|
<input
|
|
type="file"
|
|
accept="image/*"
|
|
ref={fileInputRef}
|
|
style={{ display: 'none' }}
|
|
id="newItemImageUpload" // Unique ID
|
|
/>
|
|
|
|
<Button sx={{ backgroundColor: PRIMARY_COLOR, borderBottom: '1px solid', borderColor: '#444', color: SECONDARY_COLOR }}variant="contained" color="primary" onClick={handleAddItem}>
|
|
Add Item
|
|
</Button>
|
|
|
|
{/* Dialog for adding new item */}
|
|
<Dialog open={openAddItemDialog} onClose={handleCloseAddItemDialog}>
|
|
<DialogTitle>Add New Item</DialogTitle>
|
|
<DialogContent>
|
|
<TextField
|
|
label="Item Name"
|
|
variant="outlined"
|
|
fullWidth
|
|
margin="normal"
|
|
value={newItemName}
|
|
onChange={(e) => setNewItemName(e.target.value)}
|
|
/>
|
|
<TextField
|
|
label="Item Description"
|
|
variant="outlined"
|
|
fullWidth
|
|
margin="normal"
|
|
value={newItemDescription}
|
|
onChange={(e) => setNewItemDescription(e.target.value)}
|
|
/>
|
|
<input
|
|
type="file"
|
|
accept="image/*"
|
|
ref={fileInputRef}
|
|
// capture="environment" // Capture the image from the user's camera
|
|
style={{ display: 'block', margin: '10px 0' }} // Style as needed
|
|
id="newItemImageUpload"
|
|
/>
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button onClick={handleCloseAddItemDialog}>Cancel</Button>
|
|
<Button onClick={handleSaveNewItem} color="primary">
|
|
Save
|
|
</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
{editingItem ? (
|
|
<ItemDetails
|
|
item={editingItem}
|
|
token={token}
|
|
onSave={handleSaveEdit}
|
|
onClose={handleCloseItemDetails}
|
|
boxId={boxId}
|
|
/>
|
|
) : (
|
|
<TableContainer>
|
|
<Table>
|
|
<TableHead>
|
|
<TableRow>
|
|
<TableCell style={{ width: '40px' }}>Image</TableCell>
|
|
<TableCell style={{ width: '100px' }}>Name</TableCell>
|
|
<TableCell>Description</TableCell>
|
|
<TableCell>Actions</TableCell>
|
|
</TableRow>
|
|
</TableHead>
|
|
<TableBody>
|
|
{items
|
|
.filter(item =>
|
|
item.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
item.description.toLowerCase().includes(searchQuery.toLowerCase())
|
|
)
|
|
.map((item) => (
|
|
<TableRow key={item.ID}>
|
|
<TableCell>
|
|
<Avatar src={itemImages[item.ID] || '/images/default.jpg'} />
|
|
</TableCell>
|
|
<TableCell>{item.name}</TableCell>
|
|
<TableCell>{item.description}</TableCell>
|
|
<Box display="flex" justifyContent="space-between" width="100%">
|
|
<Tooltip title="Edit Item">
|
|
<IconButton
|
|
onClick={() => handleEditItem(item)}
|
|
size="large"
|
|
sx={{ mr: 1 }}
|
|
>
|
|
<EditIcon />
|
|
</IconButton>
|
|
</Tooltip>
|
|
<Tooltip title="Delete Item">
|
|
<IconButton
|
|
onClick={() => handleDeleteItem(item.ID)}
|
|
size="large"
|
|
color="error"
|
|
>
|
|
<DeleteIcon />
|
|
</IconButton>
|
|
</Tooltip>
|
|
</Box>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</TableContainer>
|
|
)}
|
|
</Container>
|
|
);
|
|
} |