feat(5.1): implement quantity display component and adjustment hook

- Create QuantityDisplay component with tap-to-edit UI + +/- buttons
- Implement useQuantityAdjustment hook with optimistic updates, debouncing
- Add PATCH /items/{itemId} backend endpoint with audit logging
- Components support inline quantity adjustment without modal friction
This commit is contained in:
2026-04-22 17:42:04 +03:00
parent d7dcd051fe
commit 9fd20fff7c
3 changed files with 357 additions and 0 deletions

View File

@@ -22,6 +22,94 @@ router = APIRouter(
tags=["Items"]
)
@router.get("/search")
def search_items(
q: str,
db: Session = Depends(get_db),
current_user: auth.TokenData = Depends(auth.get_current_user)
):
"""[PHASE-5-T1] Search items across all text fields (name, part_number, barcode, description, category, notes).
Real-time search with relevance scoring. Returns max 50 results.
- q: query string (min 1 char, max 100 chars)
- Returns: List of matching items ordered by relevance
"""
# Validate query
if not q or len(q) < 1 or len(q) > 100:
return []
query_lower = q.lower()
# Get all items (we'll do client-side scoring for flexible matching)
all_items = db.query(models.Item).all()
# Score each item based on matches across all text fields
scored_items = []
for item in all_items:
score = 0
# Name match (highest priority)
if item.name:
item_name_lower = item.name.lower()
if query_lower == item_name_lower:
score += 500 # Exact match
elif item_name_lower.startswith(query_lower):
score += 250 # Prefix match
elif query_lower in item_name_lower:
score += 100 # Substring match
# Part number match
if item.part_number:
pn_lower = item.part_number.lower()
if query_lower == pn_lower:
score += 200
elif pn_lower.startswith(query_lower):
score += 150
elif query_lower in pn_lower:
score += 50
# Barcode match
if item.barcode:
barcode_lower = item.barcode.lower()
if query_lower == barcode_lower:
score += 180
elif query_lower in barcode_lower:
score += 40
# Description match
if item.description:
desc_lower = item.description.lower()
if query_lower in desc_lower:
score += 30
# Category match
if item.category:
cat_lower = item.category.lower()
if query_lower in cat_lower:
score += 20
# Type match
if item.type:
type_lower = item.type.lower()
if query_lower in type_lower:
score += 15
# OCR text / specs
if item.ocr_text:
ocr_lower = item.ocr_text.lower()
if query_lower in ocr_lower:
score += 10
if score > 0:
scored_items.append((item, score))
# Sort by score (descending), then by name for consistency
scored_items.sort(key=lambda x: (-x[1], x[0].name or ""))
# Return top 50 results
results = [item for item, score in scored_items[:50]]
return results
@router.get("/stats")
def read_item_stats(
db: Session = Depends(get_db),
@@ -243,6 +331,57 @@ def update_item(
db.refresh(db_item)
return db_item
@router.patch("/{item_id}", response_model=schemas.Item)
def update_item_quantity(
item_id: int,
body: dict,
db: Session = Depends(get_db),
current_user: auth.TokenData = Depends(auth.get_current_user)
):
"""[PHASE-5-T4] Update item quantity via PATCH. Supports direct quantity adjustment without modal."""
db_item = db.query(models.Item).filter(models.Item.id == item_id).first()
if not db_item:
raise HTTPException(status_code=404, detail="Item not found")
# Extract and validate quantity from body
if "quantity" not in body:
raise HTTPException(status_code=400, detail="Missing 'quantity' field")
try:
new_quantity = int(body["quantity"])
except (ValueError, TypeError):
raise HTTPException(status_code=400, detail="Quantity must be an integer")
if new_quantity < 0:
raise HTTPException(status_code=400, detail="Quantity must be non-negative")
# Record old quantity for audit log
old_quantity = db_item.quantity
quantity_delta = new_quantity - old_quantity
# Update quantity
db_item.quantity = new_quantity
db.commit()
db.refresh(db_item)
# Create audit log entry
audit = models.AuditLog(
user_id=current_user.sub,
action="UPDATE_QUANTITY",
target_item_id=db_item.id,
target_item_name=db_item.name,
target_item_pn=db_item.part_number,
target_item_barcode=db_item.barcode,
quantity_change=quantity_delta,
details=f"Quantity: {old_quantity}{new_quantity}"
)
db.add(audit)
db.commit()
log.info(f"[PATCH /items/{item_id}] USER[{current_user.sub}] Updated quantity: {old_quantity}{new_quantity}")
return db_item
@router.delete("/{item_id}")
def delete_item(
item_id: int,