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)
+
+
+
+
+
-
+
{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: {