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:
131
frontend/components/inventory/QuantityDisplay.tsx
Normal file
131
frontend/components/inventory/QuantityDisplay.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { Plus, Minus } from 'lucide-react';
|
||||
|
||||
interface QuantityDisplayProps {
|
||||
itemId: string;
|
||||
currentQuantity: number;
|
||||
onQuantityChange: (newQty: number) => Promise<void>;
|
||||
}
|
||||
|
||||
export default function QuantityDisplay({
|
||||
itemId,
|
||||
currentQuantity,
|
||||
onQuantityChange,
|
||||
}: QuantityDisplayProps) {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [localQuantity, setLocalQuantity] = useState(currentQuantity);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalQuantity(currentQuantity);
|
||||
}, [currentQuantity]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditing && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
inputRef.current.select();
|
||||
}
|
||||
}, [isEditing]);
|
||||
|
||||
const handleTapToEdit = () => {
|
||||
setIsEditing(true);
|
||||
};
|
||||
|
||||
const handleIncrement = () => {
|
||||
setLocalQuantity(prev => Math.max(0, prev + 1));
|
||||
};
|
||||
|
||||
const handleDecrement = () => {
|
||||
setLocalQuantity(prev => Math.max(0, prev - 1));
|
||||
};
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
if (value === '' || /^\d+$/.test(value)) {
|
||||
setLocalQuantity(value === '' ? 0 : parseInt(value, 10));
|
||||
}
|
||||
};
|
||||
|
||||
const handleCommit = async () => {
|
||||
if (localQuantity === currentQuantity) {
|
||||
setIsEditing(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await onQuantityChange(localQuantity);
|
||||
setIsEditing(false);
|
||||
} catch (error) {
|
||||
// Revert on error
|
||||
setLocalQuantity(currentQuantity);
|
||||
setIsEditing(false);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setLocalQuantity(currentQuantity);
|
||||
setIsEditing(false);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleCommit();
|
||||
} else if (e.key === 'Escape') {
|
||||
handleCancel();
|
||||
}
|
||||
};
|
||||
|
||||
if (isEditing) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 bg-slate-900/50 rounded-lg px-3 py-2">
|
||||
<button
|
||||
onClick={handleDecrement}
|
||||
disabled={isLoading}
|
||||
aria-label="Decrease quantity"
|
||||
className="p-1 hover:bg-slate-800 rounded text-secondary hover:text-primary transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<Minus size={16} />
|
||||
</button>
|
||||
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
value={localQuantity}
|
||||
onChange={handleInputChange}
|
||||
onBlur={handleCommit}
|
||||
onKeyDown={handleKeyDown}
|
||||
disabled={isLoading}
|
||||
aria-label="Quantity"
|
||||
className="w-12 text-center bg-transparent text-primary font-medium focus:outline-none disabled:opacity-50"
|
||||
/>
|
||||
|
||||
<button
|
||||
onClick={handleIncrement}
|
||||
disabled={isLoading}
|
||||
aria-label="Increase quantity"
|
||||
className="p-1 hover:bg-slate-800 rounded text-secondary hover:text-primary transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<Plus size={16} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleTapToEdit}
|
||||
disabled={isLoading}
|
||||
aria-label={`Quantity: ${currentQuantity}. Tap to edit`}
|
||||
className="text-lg font-medium text-primary hover:bg-slate-900/30 px-3 py-2 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{currentQuantity}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user