frontend refresh and update backend for frontend's new behavior
This commit is contained in:
parent
4139a07cd2
commit
68976a7683
@ -32,7 +32,8 @@ exports.addItem = async (req, res) => {
|
|||||||
|
|
||||||
exports.markBought = async (req, res) => {
|
exports.markBought = async (req, res) => {
|
||||||
const userId = req.user.id;
|
const userId = req.user.id;
|
||||||
await List.setBought(req.body.id, userId);
|
const { id, quantity } = req.body;
|
||||||
|
await List.setBought(id, userId, quantity);
|
||||||
res.json({ message: "Item marked bought" });
|
res.json({ message: "Item marked bought" });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -34,7 +34,30 @@ exports.getUnboughtItems = async () => {
|
|||||||
|
|
||||||
exports.getItemByName = async (itemName) => {
|
exports.getItemByName = async (itemName) => {
|
||||||
const result = await pool.query(
|
const result = await pool.query(
|
||||||
"SELECT * FROM grocery_list WHERE item_name ILIKE $1",
|
`SELECT
|
||||||
|
gl.id,
|
||||||
|
LOWER(gl.item_name) AS item_name,
|
||||||
|
gl.quantity,
|
||||||
|
gl.bought,
|
||||||
|
ENCODE(gl.item_image, 'base64') as item_image,
|
||||||
|
gl.image_mime_type,
|
||||||
|
(
|
||||||
|
SELECT ARRAY_AGG(DISTINCT u.name)
|
||||||
|
FROM (
|
||||||
|
SELECT DISTINCT gh.added_by
|
||||||
|
FROM grocery_history gh
|
||||||
|
WHERE gh.list_item_id = gl.id
|
||||||
|
ORDER BY gh.added_by
|
||||||
|
) gh
|
||||||
|
JOIN users u ON gh.added_by = u.id
|
||||||
|
) as added_by_users,
|
||||||
|
gl.modified_on as last_added_on,
|
||||||
|
ic.item_type,
|
||||||
|
ic.item_group,
|
||||||
|
ic.zone
|
||||||
|
FROM grocery_list gl
|
||||||
|
LEFT JOIN item_classification ic ON gl.id = ic.id
|
||||||
|
WHERE gl.item_name ILIKE $1`,
|
||||||
[itemName]
|
[itemName]
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -87,11 +110,31 @@ exports.addOrUpdateItem = async (itemName, quantity, userId, imageBuffer = null,
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
exports.setBought = async (id, userId) => {
|
exports.setBought = async (id, userId, quantityBought) => {
|
||||||
await pool.query(
|
// Get current item
|
||||||
"UPDATE grocery_list SET bought = TRUE, modified_on = NOW() WHERE id = $1",
|
const item = await pool.query(
|
||||||
|
"SELECT quantity FROM grocery_list WHERE id = $1",
|
||||||
[id]
|
[id]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!item.rows[0]) return;
|
||||||
|
|
||||||
|
const currentQuantity = item.rows[0].quantity;
|
||||||
|
const remainingQuantity = currentQuantity - quantityBought;
|
||||||
|
|
||||||
|
if (remainingQuantity <= 0) {
|
||||||
|
// Mark as bought if all quantity is purchased
|
||||||
|
await pool.query(
|
||||||
|
"UPDATE grocery_list SET bought = TRUE, modified_on = NOW() WHERE id = $1",
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Reduce quantity if partial purchase
|
||||||
|
await pool.query(
|
||||||
|
"UPDATE grocery_list SET quantity = $1, modified_on = NOW() WHERE id = $2",
|
||||||
|
[remainingQuantity, id]
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -106,7 +149,7 @@ exports.addHistoryRecord = async (itemId, quantity, userId) => {
|
|||||||
|
|
||||||
exports.getSuggestions = async (query) => {
|
exports.getSuggestions = async (query) => {
|
||||||
const result = await pool.query(
|
const result = await pool.query(
|
||||||
`SELECT DISTINCT item_name
|
`SELECT DISTINCT LOWER(item_name) as item_name
|
||||||
FROM grocery_list
|
FROM grocery_list
|
||||||
WHERE item_name ILIKE $1
|
WHERE item_name ILIKE $1
|
||||||
LIMIT 10`,
|
LIMIT 10`,
|
||||||
|
|||||||
@ -27,7 +27,7 @@ export const updateItemWithClassification = (id, itemName, quantity, classificat
|
|||||||
classification
|
classification
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
export const markBought = (id) => api.post("/list/mark-bought", { id });
|
export const markBought = (id, quantity) => api.post("/list/mark-bought", { id, quantity });
|
||||||
export const getSuggestions = (query) => api.get("/list/suggest", { params: { query: query } });
|
export const getSuggestions = (query) => api.get("/list/suggest", { params: { query: query } });
|
||||||
export const getRecentlyBought = () => api.get("/list/recently-bought");
|
export const getRecentlyBought = () => api.get("/list/recently-bought");
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { memo, useRef, useState } from "react";
|
|||||||
import AddImageModal from "../modals/AddImageModal";
|
import AddImageModal from "../modals/AddImageModal";
|
||||||
import ConfirmBuyModal from "../modals/ConfirmBuyModal";
|
import ConfirmBuyModal from "../modals/ConfirmBuyModal";
|
||||||
|
|
||||||
function GroceryListItem({ item, onClick, onImageAdded, onLongPress, allItems = [] }) {
|
function GroceryListItem({ item, onClick, onImageAdded, onLongPress, allItems = [], compact = false }) {
|
||||||
const [showAddImageModal, setShowAddImageModal] = useState(false);
|
const [showAddImageModal, setShowAddImageModal] = useState(false);
|
||||||
const [showConfirmBuyModal, setShowConfirmBuyModal] = useState(false);
|
const [showConfirmBuyModal, setShowConfirmBuyModal] = useState(false);
|
||||||
const [currentItem, setCurrentItem] = useState(item);
|
const [currentItem, setCurrentItem] = useState(item);
|
||||||
@ -120,7 +120,7 @@ function GroceryListItem({ item, onClick, onImageAdded, onLongPress, allItems =
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<li
|
<li
|
||||||
className="glist-li"
|
className={`glist-li ${compact ? 'compact' : ''}`}
|
||||||
onClick={handleItemClick}
|
onClick={handleItemClick}
|
||||||
onTouchStart={handleTouchStart}
|
onTouchStart={handleTouchStart}
|
||||||
onTouchMove={handleTouchMove}
|
onTouchMove={handleTouchMove}
|
||||||
|
|||||||
47
frontend/src/components/modals/ConfirmAddExistingModal.jsx
Normal file
47
frontend/src/components/modals/ConfirmAddExistingModal.jsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import "../../styles/components/ConfirmAddExistingModal.css";
|
||||||
|
|
||||||
|
export default function ConfirmAddExistingModal({
|
||||||
|
itemName,
|
||||||
|
currentQuantity,
|
||||||
|
addingQuantity,
|
||||||
|
onConfirm,
|
||||||
|
onCancel
|
||||||
|
}) {
|
||||||
|
const newQuantity = currentQuantity + addingQuantity;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="confirm-add-existing-overlay" onClick={onCancel}>
|
||||||
|
<div className="confirm-add-existing-modal" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<h2 className="confirm-add-existing-title">
|
||||||
|
<strong>{itemName}</strong> is already in your list
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="confirm-add-existing-content">
|
||||||
|
<div className="confirm-add-existing-qty-info">
|
||||||
|
<div className="qty-row">
|
||||||
|
<span className="qty-label">Current quantity:</span>
|
||||||
|
<span className="qty-value">{currentQuantity}</span>
|
||||||
|
</div>
|
||||||
|
<div className="qty-row">
|
||||||
|
<span className="qty-label">Adding:</span>
|
||||||
|
<span className="qty-value">+{addingQuantity}</span>
|
||||||
|
</div>
|
||||||
|
<div className="qty-row qty-total">
|
||||||
|
<span className="qty-label">New total:</span>
|
||||||
|
<span className="qty-value">{newQuantity}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="confirm-add-existing-actions">
|
||||||
|
<button className="confirm-add-existing-btn cancel" onClick={onCancel}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button className="confirm-add-existing-btn confirm" onClick={onConfirm}>
|
||||||
|
Update Quantity
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import "../../styles/ConfirmBuyModal.css";
|
import "../../styles/ConfirmBuyModal.css";
|
||||||
|
|
||||||
export default function ConfirmBuyModal({
|
export default function ConfirmBuyModal({
|
||||||
@ -11,6 +11,11 @@ export default function ConfirmBuyModal({
|
|||||||
const [quantity, setQuantity] = useState(item.quantity);
|
const [quantity, setQuantity] = useState(item.quantity);
|
||||||
const maxQuantity = item.quantity;
|
const maxQuantity = item.quantity;
|
||||||
|
|
||||||
|
// Update quantity when item changes (navigation)
|
||||||
|
useEffect(() => {
|
||||||
|
setQuantity(item.quantity);
|
||||||
|
}, [item.id, item.quantity]);
|
||||||
|
|
||||||
// Find current index and check for prev/next
|
// Find current index and check for prev/next
|
||||||
const currentIndex = allItems.findIndex(i => i.id === item.id);
|
const currentIndex = allItems.findIndex(i => i.id === item.id);
|
||||||
const hasPrev = currentIndex > 0;
|
const hasPrev = currentIndex > 0;
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import SortDropdown from "../components/common/SortDropdown";
|
|||||||
import AddItemForm from "../components/forms/AddItemForm";
|
import AddItemForm from "../components/forms/AddItemForm";
|
||||||
import GroceryListItem from "../components/items/GroceryListItem";
|
import GroceryListItem from "../components/items/GroceryListItem";
|
||||||
import AddItemWithDetailsModal from "../components/modals/AddItemWithDetailsModal";
|
import AddItemWithDetailsModal from "../components/modals/AddItemWithDetailsModal";
|
||||||
|
import ConfirmAddExistingModal from "../components/modals/ConfirmAddExistingModal";
|
||||||
import EditItemModal from "../components/modals/EditItemModal";
|
import EditItemModal from "../components/modals/EditItemModal";
|
||||||
import SimilarItemModal from "../components/modals/SimilarItemModal";
|
import SimilarItemModal from "../components/modals/SimilarItemModal";
|
||||||
import { ZONE_FLOW } from "../constants/classifications";
|
import { ZONE_FLOW } from "../constants/classifications";
|
||||||
@ -45,6 +46,9 @@ export default function GroceryList() {
|
|||||||
const [showEditModal, setShowEditModal] = useState(false);
|
const [showEditModal, setShowEditModal] = useState(false);
|
||||||
const [editingItem, setEditingItem] = useState(null);
|
const [editingItem, setEditingItem] = useState(null);
|
||||||
const [recentlyBoughtCollapsed, setRecentlyBoughtCollapsed] = useState(settings.recentlyBoughtCollapsed);
|
const [recentlyBoughtCollapsed, setRecentlyBoughtCollapsed] = useState(settings.recentlyBoughtCollapsed);
|
||||||
|
const [collapsedZones, setCollapsedZones] = useState({});
|
||||||
|
const [showConfirmAddExisting, setShowConfirmAddExisting] = useState(false);
|
||||||
|
const [confirmAddExistingData, setConfirmAddExistingData] = useState(null);
|
||||||
|
|
||||||
|
|
||||||
// === Data Loading ===
|
// === Data Loading ===
|
||||||
@ -74,6 +78,14 @@ export default function GroceryList() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
|
// === Zone Collapse Handler ===
|
||||||
|
const toggleZoneCollapse = (zone) => {
|
||||||
|
setCollapsedZones(prev => ({
|
||||||
|
...prev,
|
||||||
|
[zone]: !prev[zone]
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
// === Sorted Items Computation ===
|
// === Sorted Items Computation ===
|
||||||
const sortedItems = useMemo(() => {
|
const sortedItems = useMemo(() => {
|
||||||
const sorted = [...items];
|
const sorted = [...items];
|
||||||
@ -125,17 +137,19 @@ export default function GroceryList() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const allItems = [...items, ...recentlyBoughtItems];
|
|
||||||
const lowerText = text.toLowerCase().trim();
|
const lowerText = text.toLowerCase().trim();
|
||||||
const exactMatch = allItems.find(item => item.item_name.toLowerCase() === lowerText);
|
|
||||||
|
|
||||||
setButtonText(exactMatch ? "Add" : "Create + Add");
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const suggestions = await getSuggestions(text);
|
const response = await getSuggestions(text);
|
||||||
setSuggestions(suggestions.data.map(s => s.item_name));
|
const suggestionList = response.data.map(s => s.item_name);
|
||||||
|
setSuggestions(suggestionList);
|
||||||
|
|
||||||
|
// All suggestions are now lowercase from DB, direct comparison
|
||||||
|
const exactMatch = suggestionList.includes(lowerText);
|
||||||
|
setButtonText(exactMatch ? "Add" : "Create + Add");
|
||||||
} catch {
|
} catch {
|
||||||
setSuggestions([]);
|
setSuggestions([]);
|
||||||
|
setButtonText("Create + Add");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -184,31 +198,24 @@ export default function GroceryList() {
|
|||||||
if (existingItem?.bought === false) {
|
if (existingItem?.bought === false) {
|
||||||
const currentQuantity = existingItem.quantity;
|
const currentQuantity = existingItem.quantity;
|
||||||
const newQuantity = currentQuantity + quantity;
|
const newQuantity = currentQuantity + quantity;
|
||||||
const yes = window.confirm(
|
|
||||||
`Item "${itemName}" already exists in the list. Do you want to update its quantity from ${currentQuantity} to ${newQuantity}?`
|
|
||||||
);
|
|
||||||
if (!yes) return;
|
|
||||||
|
|
||||||
const response = await addItem(itemName, newQuantity, null);
|
// Show modal instead of window.confirm
|
||||||
setSuggestions([]);
|
setConfirmAddExistingData({
|
||||||
setButtonText("Add Item");
|
itemName,
|
||||||
|
currentQuantity,
|
||||||
if (response.data) {
|
addingQuantity: quantity,
|
||||||
setItems(prevItems =>
|
newQuantity,
|
||||||
prevItems.map(item =>
|
existingItem
|
||||||
item.id === existingItem.id ? { ...item, ...response.data } : item
|
});
|
||||||
)
|
setShowConfirmAddExisting(true);
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if (existingItem) {
|
} else if (existingItem) {
|
||||||
const response = await addItem(itemName, quantity, null);
|
await addItem(itemName, quantity, null);
|
||||||
setSuggestions([]);
|
setSuggestions([]);
|
||||||
setButtonText("Add Item");
|
setButtonText("Add Item");
|
||||||
|
|
||||||
if (response.data) {
|
// Reload lists to reflect the changes
|
||||||
setItems(prevItems => [...prevItems, response.data]);
|
await loadItems();
|
||||||
setRecentlyBoughtItems(prevItems => prevItems.filter(item => item.id !== existingItem.id));
|
await loadRecentlyBought();
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
setPendingItem({ itemName, quantity });
|
setPendingItem({ itemName, quantity });
|
||||||
setShowAddDetailsModal(true);
|
setShowAddDetailsModal(true);
|
||||||
@ -239,6 +246,45 @@ export default function GroceryList() {
|
|||||||
}, [similarItemSuggestion, processItemAddition]);
|
}, [similarItemSuggestion, processItemAddition]);
|
||||||
|
|
||||||
|
|
||||||
|
// === Confirm Add Existing Modal Handlers ===
|
||||||
|
const handleConfirmAddExisting = useCallback(async () => {
|
||||||
|
if (!confirmAddExistingData) return;
|
||||||
|
|
||||||
|
const { itemName, newQuantity, existingItem } = confirmAddExistingData;
|
||||||
|
|
||||||
|
setShowConfirmAddExisting(false);
|
||||||
|
setConfirmAddExistingData(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Update the item
|
||||||
|
await addItem(itemName, newQuantity, null);
|
||||||
|
|
||||||
|
// Fetch the updated item with properly formatted data
|
||||||
|
const response = await getItemByName(itemName);
|
||||||
|
const updatedItem = response.data;
|
||||||
|
|
||||||
|
// Update state with the full item data
|
||||||
|
setItems(prevItems =>
|
||||||
|
prevItems.map(item =>
|
||||||
|
item.id === existingItem.id ? updatedItem : item
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
setSuggestions([]);
|
||||||
|
setButtonText("Add Item");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to update item:", error);
|
||||||
|
// Fallback to full reload on error
|
||||||
|
await loadItems();
|
||||||
|
}
|
||||||
|
}, [confirmAddExistingData, loadItems]);
|
||||||
|
|
||||||
|
const handleCancelAddExisting = useCallback(() => {
|
||||||
|
setShowConfirmAddExisting(false);
|
||||||
|
setConfirmAddExistingData(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
// === Add Details Modal Handlers ===
|
// === Add Details Modal Handlers ===
|
||||||
const handleAddDetailsConfirm = useCallback(async (imageFile, classification) => {
|
const handleAddDetailsConfirm = useCallback(async (imageFile, classification) => {
|
||||||
if (!pendingItem) return;
|
if (!pendingItem) return;
|
||||||
@ -300,10 +346,26 @@ export default function GroceryList() {
|
|||||||
|
|
||||||
// === Item Action Handlers ===
|
// === Item Action Handlers ===
|
||||||
const handleBought = useCallback(async (id, quantity) => {
|
const handleBought = useCallback(async (id, quantity) => {
|
||||||
await markBought(id);
|
const item = items.find(i => i.id === id);
|
||||||
setItems(prevItems => prevItems.filter(item => item.id !== id));
|
if (!item) return;
|
||||||
|
|
||||||
|
await markBought(id, quantity);
|
||||||
|
|
||||||
|
// If buying full quantity, remove from list
|
||||||
|
if (quantity >= item.quantity) {
|
||||||
|
setItems(prevItems => prevItems.filter(item => item.id !== id));
|
||||||
|
} else {
|
||||||
|
// If partial, update quantity
|
||||||
|
const response = await getItemByName(item.item_name);
|
||||||
|
if (response.data) {
|
||||||
|
setItems(prevItems =>
|
||||||
|
prevItems.map(item => item.id === id ? response.data : item)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
loadRecentlyBought();
|
loadRecentlyBought();
|
||||||
}, []);
|
}, [items]);
|
||||||
|
|
||||||
|
|
||||||
const handleImageAdded = useCallback(async (id, itemName, quantity, imageFile) => {
|
const handleImageAdded = useCallback(async (id, itemName, quantity, imageFile) => {
|
||||||
@ -415,39 +477,56 @@ export default function GroceryList() {
|
|||||||
{sortMode === "zone" ? (
|
{sortMode === "zone" ? (
|
||||||
(() => {
|
(() => {
|
||||||
const grouped = groupItemsByZone(sortedItems);
|
const grouped = groupItemsByZone(sortedItems);
|
||||||
return Object.keys(grouped).map(zone => (
|
return Object.keys(grouped).map(zone => {
|
||||||
<div key={zone} className="glist-classification-group">
|
const isCollapsed = collapsedZones[zone];
|
||||||
<h3 className="glist-classification-header">
|
const itemCount = grouped[zone].length;
|
||||||
{zone === 'unclassified' ? 'Unclassified' : zone}
|
return (
|
||||||
</h3>
|
<div key={zone} className="glist-classification-group">
|
||||||
<ul className="glist-ul">
|
<h3
|
||||||
{grouped[zone].map((item) => (
|
className="glist-classification-header clickable"
|
||||||
<GroceryListItem
|
onClick={() => toggleZoneCollapse(zone)}
|
||||||
key={item.id}
|
>
|
||||||
item={item}
|
<span>
|
||||||
allItems={sortedItems}
|
{zone === 'unclassified' ? 'Unclassified' : zone}
|
||||||
onClick={(id, quantity) =>
|
<span className="glist-zone-count"> ({itemCount})</span>
|
||||||
[ROLES.ADMIN, ROLES.EDITOR].includes(role) && handleBought(id, quantity)
|
</span>
|
||||||
}
|
<span className="glist-zone-indicator">
|
||||||
onImageAdded={
|
{isCollapsed ? "▼" : "▲"}
|
||||||
[ROLES.ADMIN, ROLES.EDITOR].includes(role) ? handleImageAdded : null
|
</span>
|
||||||
}
|
</h3>
|
||||||
onLongPress={
|
{!isCollapsed && (
|
||||||
[ROLES.ADMIN, ROLES.EDITOR].includes(role) ? handleLongPress : null
|
<ul className={`glist-ul ${settings.compactView ? 'compact' : ''}`}>
|
||||||
}
|
{grouped[zone].map((item) => (
|
||||||
/>
|
<GroceryListItem
|
||||||
))}
|
key={item.id}
|
||||||
</ul>
|
item={item}
|
||||||
</div>
|
allItems={sortedItems}
|
||||||
));
|
compact={settings.compactView}
|
||||||
|
onClick={(id, quantity) =>
|
||||||
|
[ROLES.ADMIN, ROLES.EDITOR].includes(role) && handleBought(id, quantity)
|
||||||
|
}
|
||||||
|
onImageAdded={
|
||||||
|
[ROLES.ADMIN, ROLES.EDITOR].includes(role) ? handleImageAdded : null
|
||||||
|
}
|
||||||
|
onLongPress={
|
||||||
|
[ROLES.ADMIN, ROLES.EDITOR].includes(role) ? handleLongPress : null
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
})()
|
})()
|
||||||
) : (
|
) : (
|
||||||
<ul className="glist-ul">
|
<ul className={`glist-ul ${settings.compactView ? 'compact' : ''}`}>
|
||||||
{sortedItems.map((item) => (
|
{sortedItems.map((item) => (
|
||||||
<GroceryListItem
|
<GroceryListItem
|
||||||
key={item.id}
|
key={item.id}
|
||||||
item={item}
|
item={item}
|
||||||
allItems={sortedItems}
|
allItems={sortedItems}
|
||||||
|
compact={settings.compactView}
|
||||||
onClick={(id, quantity) =>
|
onClick={(id, quantity) =>
|
||||||
[ROLES.ADMIN, ROLES.EDITOR].includes(role) && handleBought(id, quantity)
|
[ROLES.ADMIN, ROLES.EDITOR].includes(role) && handleBought(id, quantity)
|
||||||
}
|
}
|
||||||
@ -464,24 +543,25 @@ export default function GroceryList() {
|
|||||||
|
|
||||||
{recentlyBoughtItems.length > 0 && settings.showRecentlyBought && (
|
{recentlyBoughtItems.length > 0 && settings.showRecentlyBought && (
|
||||||
<>
|
<>
|
||||||
<div className="glist-section-header">
|
<h2
|
||||||
<h2 className="glist-section-title">Recently Bought (24HR)</h2>
|
className="glist-section-title clickable"
|
||||||
<button
|
onClick={() => setRecentlyBoughtCollapsed(!recentlyBoughtCollapsed)}
|
||||||
className="glist-collapse-btn"
|
>
|
||||||
onClick={() => setRecentlyBoughtCollapsed(!recentlyBoughtCollapsed)}
|
<span>Recently Bought (24HR)</span>
|
||||||
>
|
<span className="glist-section-indicator">
|
||||||
{recentlyBoughtCollapsed ? "▼ Show" : "▲ Hide"}
|
{recentlyBoughtCollapsed ? "▼" : "▲"}
|
||||||
</button>
|
</span>
|
||||||
</div>
|
</h2>
|
||||||
|
|
||||||
{!recentlyBoughtCollapsed && (
|
{!recentlyBoughtCollapsed && (
|
||||||
<>
|
<>
|
||||||
<ul className="glist-ul">
|
<ul className={`glist-ul ${settings.compactView ? 'compact' : ''}`}>
|
||||||
{recentlyBoughtItems.slice(0, recentlyBoughtDisplayCount).map((item) => (
|
{recentlyBoughtItems.slice(0, recentlyBoughtDisplayCount).map((item) => (
|
||||||
<GroceryListItem
|
<GroceryListItem
|
||||||
key={item.id}
|
key={item.id}
|
||||||
item={item}
|
item={item}
|
||||||
allItems={recentlyBoughtItems}
|
allItems={recentlyBoughtItems}
|
||||||
|
compact={settings.compactView}
|
||||||
onClick={null}
|
onClick={null}
|
||||||
onImageAdded={
|
onImageAdded={
|
||||||
[ROLES.ADMIN, ROLES.EDITOR].includes(role) ? handleImageAdded : null
|
[ROLES.ADMIN, ROLES.EDITOR].includes(role) ? handleImageAdded : null
|
||||||
@ -542,6 +622,16 @@ export default function GroceryList() {
|
|||||||
onImageUpdate={handleImageAdded}
|
onImageUpdate={handleImageAdded}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{showConfirmAddExisting && confirmAddExistingData && (
|
||||||
|
<ConfirmAddExistingModal
|
||||||
|
itemName={confirmAddExistingData.itemName}
|
||||||
|
currentQuantity={confirmAddExistingData.currentQuantity}
|
||||||
|
addingQuantity={confirmAddExistingData.addingQuantity}
|
||||||
|
onConfirm={handleConfirmAddExisting}
|
||||||
|
onCancel={handleCancelAddExisting}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
144
frontend/src/styles/components/ConfirmAddExistingModal.css
Normal file
144
frontend/src/styles/components/ConfirmAddExistingModal.css
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
/* Confirm Add Existing Modal */
|
||||||
|
.confirm-add-existing-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: var(--modal-backdrop-bg);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: var(--z-modal);
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-add-existing-modal {
|
||||||
|
background: var(--modal-bg);
|
||||||
|
border-radius: var(--modal-border-radius);
|
||||||
|
padding: var(--modal-padding);
|
||||||
|
max-width: 400px;
|
||||||
|
width: 100%;
|
||||||
|
box-shadow: var(--shadow-xl);
|
||||||
|
animation: slideIn 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-add-existing-title {
|
||||||
|
margin: 0 0 var(--spacing-lg) 0;
|
||||||
|
font-size: var(--font-size-xl);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
text-align: center;
|
||||||
|
font-weight: var(--font-weight-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-add-existing-title strong {
|
||||||
|
color: var(--color-primary);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-add-existing-content {
|
||||||
|
margin-bottom: var(--spacing-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-add-existing-qty-info {
|
||||||
|
background: var(--color-bg-surface);
|
||||||
|
border: var(--border-width-thin) solid var(--color-border-light);
|
||||||
|
border-radius: var(--border-radius-md);
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
margin: var(--spacing-md) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qty-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: var(--spacing-xs) 0;
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.qty-row.qty-total {
|
||||||
|
margin-top: var(--spacing-sm);
|
||||||
|
padding-top: var(--spacing-sm);
|
||||||
|
border-top: var(--border-width-medium) solid var(--color-border-medium);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.qty-label {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.qty-value {
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
font-size: var(--font-size-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.qty-total .qty-label,
|
||||||
|
.qty-total .qty-value {
|
||||||
|
color: var(--color-primary);
|
||||||
|
font-size: var(--font-size-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-add-existing-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-md);
|
||||||
|
margin-top: var(--spacing-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-add-existing-btn {
|
||||||
|
flex: 1;
|
||||||
|
padding: var(--button-padding-y) var(--button-padding-x);
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--button-border-radius);
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
font-weight: var(--button-font-weight);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: var(--transition-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-add-existing-btn.cancel {
|
||||||
|
background: var(--color-bg-surface);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
border: var(--border-width-thin) solid var(--color-border-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-add-existing-btn.cancel:hover {
|
||||||
|
background: var(--color-bg-hover);
|
||||||
|
border-color: var(--color-border-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-add-existing-btn.confirm {
|
||||||
|
background: var(--color-primary);
|
||||||
|
color: var(--color-text-inverse);
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-add-existing-btn.confirm:hover {
|
||||||
|
background: var(--color-primary-hover);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-add-existing-btn.confirm:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.confirm-add-existing-modal {
|
||||||
|
max-width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-add-existing-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -29,6 +29,30 @@
|
|||||||
color: var(--color-gray-700);
|
color: var(--color-gray-700);
|
||||||
border-top: var(--border-width-medium) solid var(--color-border-light);
|
border-top: var(--border-width-medium) solid var(--color-border-light);
|
||||||
padding-top: var(--spacing-md);
|
padding-top: var(--spacing-md);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glist-section-title.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: var(--transition-base);
|
||||||
|
user-select: none;
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
background: var(--color-bg-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glist-section-title.clickable:hover {
|
||||||
|
background: var(--color-bg-hover);
|
||||||
|
color: var(--color-primary);
|
||||||
|
border-top-color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glist-section-indicator {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
opacity: 0.7;
|
||||||
|
margin-left: var(--spacing-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.glist-section-header {
|
.glist-section-header {
|
||||||
@ -79,6 +103,34 @@
|
|||||||
background: var(--color-primary-light);
|
background: var(--color-primary-light);
|
||||||
border-left: var(--border-width-thick) solid var(--color-primary);
|
border-left: var(--border-width-thick) solid var(--color-primary);
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glist-classification-header.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: var(--transition-base);
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glist-classification-header.clickable:hover {
|
||||||
|
background: var(--color-primary);
|
||||||
|
color: var(--color-text-inverse);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glist-zone-count {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-weight: var(--font-weight-normal);
|
||||||
|
opacity: 0.8;
|
||||||
|
margin-left: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glist-zone-indicator {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
opacity: 0.7;
|
||||||
|
margin-left: var(--spacing-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Inputs */
|
/* Inputs */
|
||||||
@ -211,11 +263,6 @@
|
|||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
.glist-item-image.has-image:hover {
|
|
||||||
opacity: 0.8;
|
|
||||||
box-shadow: 0 0 8px var(--color-primary-light);
|
|
||||||
}
|
|
||||||
|
|
||||||
.glist-item-content {
|
.glist-item-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -258,6 +305,35 @@
|
|||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Compact View */
|
||||||
|
.glist-ul.compact .glist-li {
|
||||||
|
padding: var(--spacing-xs);
|
||||||
|
margin-bottom: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glist-ul.compact .glist-item-layout {
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glist-ul.compact .glist-item-image {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
font-size: var(--font-size-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glist-ul.compact .glist-item-quantity {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
padding: 1px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glist-ul.compact .glist-item-name {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glist-ul.compact .glist-item-users {
|
||||||
|
font-size: 0.65em;
|
||||||
|
}
|
||||||
|
|
||||||
/* Sorting dropdown */
|
/* Sorting dropdown */
|
||||||
.glist-sort {
|
.glist-sort {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user