- 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
88 lines
2.4 KiB
TypeScript
88 lines
2.4 KiB
TypeScript
import { useState, useCallback, useRef } from 'react';
|
|
import axios from 'axios';
|
|
|
|
interface UseQuantityAdjustmentResult {
|
|
quantity: number;
|
|
isLoading: boolean;
|
|
error: string | null;
|
|
adjustQuantity: (newQuantity: number) => Promise<void>;
|
|
resetError: () => void;
|
|
}
|
|
|
|
export function useQuantityAdjustment(
|
|
itemId: string,
|
|
initialQuantity: number
|
|
): UseQuantityAdjustmentResult {
|
|
const [quantity, setQuantity] = useState(initialQuantity);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
|
|
|
|
const adjustQuantity = useCallback(
|
|
async (newQuantity: number) => {
|
|
// Validate quantity
|
|
if (typeof newQuantity !== 'number' || newQuantity < 0 || !Number.isInteger(newQuantity)) {
|
|
setError('Quantity must be a non-negative integer');
|
|
return;
|
|
}
|
|
|
|
// Clear any pending debounce timer
|
|
if (debounceTimerRef.current) {
|
|
clearTimeout(debounceTimerRef.current);
|
|
}
|
|
|
|
// Optimistic update
|
|
const previousQuantity = quantity;
|
|
setQuantity(newQuantity);
|
|
setError(null);
|
|
setIsLoading(true);
|
|
|
|
try {
|
|
// Debounce: wait 100ms before sending to API
|
|
await new Promise(resolve => {
|
|
debounceTimerRef.current = setTimeout(resolve, 100);
|
|
});
|
|
|
|
// Make API call
|
|
const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8906';
|
|
const response = await axios.patch(`${backendUrl}/items/${itemId}`, {
|
|
quantity: newQuantity,
|
|
});
|
|
|
|
// Confirm the update with response data
|
|
if (response.data && typeof response.data.quantity === 'number') {
|
|
setQuantity(response.data.quantity);
|
|
}
|
|
} catch (err) {
|
|
// Revert optimistic update on error
|
|
setQuantity(previousQuantity);
|
|
|
|
const errorMessage =
|
|
err instanceof Error
|
|
? err.message
|
|
: axios.isAxiosError(err)
|
|
? err.response?.data?.detail || 'Failed to update quantity'
|
|
: 'Failed to update quantity';
|
|
|
|
setError(errorMessage);
|
|
throw err;
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
},
|
|
[itemId, quantity]
|
|
);
|
|
|
|
const resetError = useCallback(() => {
|
|
setError(null);
|
|
}, []);
|
|
|
|
return {
|
|
quantity,
|
|
isLoading,
|
|
error,
|
|
adjustQuantity,
|
|
resetError,
|
|
};
|
|
}
|