From e0f7af5e881dd289065dee7af96e09c408e1bea0 Mon Sep 17 00:00:00 2001 From: Steve White Date: Fri, 1 Nov 2024 23:47:37 -0500 Subject: [PATCH] GUI improvements --- src/components/Admin.js | 302 ++++++++++++++++++++++++++-------------- src/components/Items.js | 232 ++++++++++++++++++------------ src/services/api.js | 16 ++- 3 files changed, 345 insertions(+), 205 deletions(-) diff --git a/src/components/Admin.js b/src/components/Admin.js index 4d50596..0df0b80 100644 --- a/src/components/Admin.js +++ b/src/components/Admin.js @@ -1,5 +1,5 @@ import React, { useState, useEffect, useRef } from 'react'; -import axios from 'axios'; +// import axios from 'axios'; import { Link, useNavigate } from 'react-router-dom'; import { PRIMARY_COLOR, SECONDARY_COLOR } from '../App'; import './Admin.css'; @@ -19,33 +19,41 @@ import { Container, Box, TextField, - CircularProgress + CircularProgress, + Tab, + Tabs } from '@mui/material'; export default function Admin() { const [users, setUsers] = useState([]); + const [tags, setTags] = useState([]); const [username, setUsername] = useState(''); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); + const [activeTab, setActiveTab] = useState(0); const navigate = useNavigate(); const fileInputRef = useRef(null); const { execute: fetchUsers, loading: loadingUsers, error: usersError } = useApiCall(); + const { execute: fetchTags, loading: loadingTags, error: tagsError } = useApiCall(); const { execute: createUser, loading: creatingUser, error: createError } = useApiCall(); const { execute: deleteUser, loading: deletingUser, error: deleteError } = useApiCall(); + const { execute: deleteTag, loading: deletingTag, error: deleteTagError } = useApiCall(); const { execute: backupDB, loading: backingUp, error: backupError } = useApiCall(); const { execute: restoreDB, loading: restoring, error: restoreError } = useApiCall(); useEffect(() => { - const getUsers = async () => { + const getInitialData = async () => { try { - const response = await fetchUsers(() => - api.admin.getUsers(localStorage.getItem('token')) - ); - setUsers(response.data); + const [usersResponse, tagsResponse] = await Promise.all([ + fetchUsers(() => api.admin.getUsers(localStorage.getItem('token'))), + fetchTags(() => api.tags.getAll(localStorage.getItem('token'))) + ]); + setUsers(usersResponse.data); + setTags(tagsResponse.data); } catch (err) {} }; - getUsers(); + getInitialData(); }, []); const handleCreateUser = async (e) => { @@ -73,6 +81,19 @@ export default function Admin() { } catch (err) {} }; + const handleDeleteTag = async (tagId) => { + if (!window.confirm('Are you sure you want to delete this tag? This will remove it from all items.')) { + return; + } + + try { + await deleteTag(() => + api.tags.delete(localStorage.getItem('token'), tagId) + ); + setTags(tags.filter(tag => tag.ID !== tagId)); + } catch (err) {} + }; + const handleBackupDatabase = async () => { try { const response = await backupDB(() => @@ -109,113 +130,180 @@ export default function Admin() { Admin Dashboard - {(usersError || createError || deleteError || backupError || restoreError) && ( + {(usersError || createError || deleteError || backupError || restoreError || tagsError || deleteTagError) && ( {usersError?.message || createError?.message || deleteError?.message || - backupError?.message || restoreError?.message} + backupError?.message || restoreError?.message || tagsError?.message || deleteTagError?.message} )} - - - - Add New User - - setUsername(e.target.value)} - sx={{ mr: 2 }} - disabled={creatingUser} - /> - setPassword(e.target.value)} - sx={{ mr: 2 }} - disabled={creatingUser} - /> - setEmail(e.target.value)} - sx={{ mr: 2 }} - disabled={creatingUser} - /> - - - {loadingUsers ? ( - - ) : ( - - - - - ID - Username - Email - Actions - - - - {users.map(user => ( - - {user.ID} - {user.username} - {user.email} - - - - - ))} - -
-
+ setActiveTab(newValue)} sx={{ mb: 3 }}> + + + + + + {activeTab === 0 && ( + <> + + + Add New User + + setUsername(e.target.value)} + sx={{ mr: 2 }} + disabled={creatingUser} + /> + setPassword(e.target.value)} + sx={{ mr: 2 }} + disabled={creatingUser} + /> + setEmail(e.target.value)} + sx={{ mr: 2 }} + disabled={creatingUser} + /> + + + + {loadingUsers ? ( + + ) : ( + + + + + ID + Username + Email + Actions + + + + {users.map(user => ( + + {user.ID} + {user.username} + {user.email} + + + + + ))} + +
+
+ )} + )} - - + {activeTab === 1 && ( + <> + + Manage Tags + + {loadingTags ? ( + + ) : ( + + + + + Color + Name + Description + Actions + + + + {tags.map(tag => ( + + + + + {tag.name} + {tag.description} + + + + + ))} + +
+
+ )} + + )} - -
+ {activeTab === 2 && ( + + + Database Management + + + + + + )} ); } \ No newline at end of file diff --git a/src/components/Items.js b/src/components/Items.js index 56e6ffe..09b8aba 100644 --- a/src/components/Items.js +++ b/src/components/Items.js @@ -19,7 +19,9 @@ import { DialogActions, Alert, CircularProgress, - Chip + Chip, + Typography, + FormGroup } from '@mui/material'; import { Delete as DeleteIcon, Edit as EditIcon } from '@mui/icons-material'; import { useParams, useLocation } from 'react-router-dom'; @@ -29,8 +31,6 @@ import { api } from '../services/api'; import ItemDetails from './ItemDetails'; import TagManager from './tagManager'; - -// In Items.js, modify the Item component const Item = React.memo(({ item, onDelete, onEdit, itemImages }) => ( @@ -38,7 +38,7 @@ const Item = React.memo(({ item, onDelete, onEdit, itemImages }) => ( {item.name} {item.description} - {/* Add this cell */} + {item.tags?.map(tag => ( { - const getItems = async () => { + const loadData = async () => { try { - const response = await fetchItems(() => - boxId ? api.items.getByBox(token, boxId) : api.items.getAll(token) - ); - setItems(response.data); + // Fetch items and tags in parallel + const [itemsResponse, tagsResponse] = await Promise.all([ + fetchItems(() => boxId ? api.items.getByBox(token, boxId) : api.items.getAll(token)), + fetchTags(() => api.tags.getAll(token)) + ]); + + setItems(itemsResponse.data); + setAvailableTags(tagsResponse.data); // Fetch images for each item - response.data.forEach(item => { + itemsResponse.data.forEach(item => { api.items.getImage(token, item.ID) .then(response => { const reader = new FileReader(); @@ -121,30 +125,80 @@ export default function Items({ token }) { }); } catch (err) {} }; - getItems(); + loadData(); }, [token, boxId]); - const handleAddItem = () => { - setOpenAddItemDialog(true); + // Filter items based on search query and selected tags + const filteredItems = useMemo(() => { + return items.filter(item => { + // Text search match + const textMatch = + item.name.toLowerCase().includes(searchQuery.toLowerCase()) || + item.description.toLowerCase().includes(searchQuery.toLowerCase()); + + // Tag match - if no tags selected, show all items + const tagMatch = selectedTags.length === 0 || + selectedTags.every(tagId => + item.tags?.some(itemTag => itemTag.ID === tagId) + ); + + return textMatch && tagMatch; + }); + }, [items, searchQuery, selectedTags]); + + // Handle tag selection + const handleTagSelect = (tagId) => { + setSelectedTags(prev => + prev.includes(tagId) + ? prev.filter(id => id !== tagId) + : [...prev, tagId] + ); }; - const handleCloseAddItemDialog = () => { + // Rest of the component remains the same... + // (keeping all existing functions like handleAddItem, handleDeleteItem, etc.) + const handleAddItem = useCallback(() => { + setOpenAddItemDialog(true); + }, []); + + const handleCloseAddItemDialog = useCallback(() => { setOpenAddItemDialog(false); setNewItemName(''); setNewItemDescription(''); if (fileInputRef.current) { fileInputRef.current.value = ''; } - }; + }, []); - const generateUniqueImageName = (imageName) => { + const generateUniqueImageName = useCallback((imageName) => { if (imageName.toLowerCase() === 'image.jpg') { const randomString = Math.random().toString(36).substr(2, 9); return `image_${randomString}.jpg`; } return imageName; - }; + }, []); + const handleDeleteItem = useCallback(async (itemId) => { + try { + await deleteItem(() => api.items.delete(token, itemId)); + setItems(prev => prev.filter(item => item.ID !== itemId)); + } catch (err) {} + }, [token, deleteItem]); + const handleEditItem = useCallback((item) => { + setEditingItem(item); + }, []); + + const handleSaveEdit = useCallback(async () => { + setEditingItem(null); + // Refresh the items list after editing + try { + const response = await fetchItems(() => + boxId ? api.items.getByBox(token, boxId) : api.items.getAll(token) + ); + setItems(response.data); + } catch (err) {} + }, [token, boxId, fetchItems]); + const handleSaveNewItem = useCallback(async () => { try { const newItemResponse = await createItem(() => @@ -193,66 +247,63 @@ export default function Items({ token }) { ); setItems(response.data); } catch (err) {} - }, [token, boxId, newItemName, newItemDescription, createItem, uploadImage, fetchItems]); - - const handleDeleteItem = useCallback(async (itemId) => { - try { - await deleteItem(() => api.items.delete(token, itemId)); - setItems(prev => prev.filter(item => item.ID !== itemId)); - } catch (err) {} - }, [token, deleteItem]); - - const handleEditItem = useCallback((item) => { - setEditingItem(item); - }, []); - - const handleSaveEdit = async () => { - setEditingItem(null); - // Refresh the items list after editing - try { - const response = await fetchItems(() => - boxId ? api.items.getByBox(token, boxId) : api.items.getAll(token) - ); - setItems(response.data); - } catch (err) {} - }; - - const filteredItems = useMemo(() => - items.filter(item => - item.name.toLowerCase().includes(searchQuery.toLowerCase()) || - item.description.toLowerCase().includes(searchQuery.toLowerCase()) - ), - [items, searchQuery] - ); + }, [token, boxId, newItemName, newItemDescription, createItem, uploadImage, fetchItems, generateUniqueImageName]); return ( - {(itemsError || createError || deleteError || uploadError) && ( + {(itemsError || tagsError || createError || deleteError || uploadError) && ( - {itemsError?.message || createError?.message || deleteError?.message || uploadError?.message} + {itemsError?.message || tagsError?.message || createError?.message || + deleteError?.message || uploadError?.message} )} - - setSearchQuery(e.target.value)} - /> - -

Items in Box: {boxName === "Unknown Box" ? "All Boxes" : `${boxName} (${items.length} items)`}

- - - + + + setSearchQuery(e.target.value)} + /> + + + Filter by tags: + + {availableTags.map(tag => ( + handleTagSelect(tag.ID)} + clickable + /> + ))} + + + + + + + {boxName === "All Boxes" ? "All Items" : `Items in ${boxName}`} + ({filteredItems.length} items) + + + + + Add New Item @@ -290,7 +341,7 @@ export default function Items({ token }) { - + {editingItem && ( )} - + {loadingItems ? ( ) : ( @@ -315,20 +366,19 @@ export default function Items({ token }) { Actions
- - {filteredItems.map((item) => ( - - ))} - + + {filteredItems.map((item) => ( + + ))} + )} - ); -} \ No newline at end of file + )}; \ No newline at end of file diff --git a/src/services/api.js b/src/services/api.js index b595c12..a0448c2 100644 --- a/src/services/api.js +++ b/src/services/api.js @@ -27,11 +27,7 @@ const createApiClient = () => { client.delete(`/api/v1/items/${id}`, { headers: authHeader(token) }), uploadImage: (token, id, formData) => client.post(`/api/v1/items/${id}/upload`, formData, { - headers: { ...authHeader(token), 'Content-Type': 'multipart/form-data' }, - addTags: (token, id, tagIds) => - client.post(`/api/v1/items/${id}/tags`, tagIds, { headers: authHeader(token) }), - removeTag: (token, id, tagId) => - client.delete(`/api/v1/items/${id}/tags/${tagId}`, { headers: authHeader(token) }), + headers: { ...authHeader(token), 'Content-Type': 'multipart/form-data' } }), getImage: (token, id) => client.get(`/api/v1/items/${id}/image`, { @@ -40,8 +36,15 @@ const createApiClient = () => { }), getByBox: (token, boxId) => client.get(`/api/v1/boxes/${boxId}/items`, { headers: authHeader(token) }), + addTags: (token, itemId, tagIds) => + client.post(`/api/v1/items/${itemId}/tags`, tagIds, { + headers: authHeader(token) + }), + removeTag: (token, itemId, tagId) => + client.delete(`/api/v1/items/${itemId}/tags/${tagId}`, { + headers: authHeader(token) + }), }, - // Boxes boxes: { getAll: (token) => @@ -64,7 +67,6 @@ const createApiClient = () => { getItems: (token, id) => client.get(`/api/v1/tags/${id}/items`, { headers: authHeader(token) }), }, - // Admin admin: {