toast.warning is not a valid method in react-hot-toast API. Changed to toast.success since the item was successfully created even if photo upload failed.
243 lines
7.1 KiB
TypeScript
243 lines
7.1 KiB
TypeScript
import { useState, useCallback } from 'react';
|
|
import toast from 'react-hot-toast';
|
|
import { inventoryApi } from '@/lib/api';
|
|
import { CropBounds } from './useCropHandles';
|
|
|
|
interface ItemFormData {
|
|
name: string;
|
|
category: string;
|
|
item_type: string;
|
|
quantity: number;
|
|
barcode?: string;
|
|
part_number?: string;
|
|
box_label?: string;
|
|
extractedImageBlob?: Blob;
|
|
imageProcessing?: {
|
|
crop_bounds?: { x: number; y: number; width: number; height: number };
|
|
rotation_degrees?: number;
|
|
confidence?: number;
|
|
};
|
|
}
|
|
|
|
interface UploadedPhoto {
|
|
thumbnail_url: string;
|
|
full_url: string;
|
|
uploaded_at: string;
|
|
}
|
|
|
|
interface UseItemCreateReturn {
|
|
step: 'details' | 'photo' | 'preview' | 'confirm';
|
|
formData: ItemFormData;
|
|
setFormData: (data: Partial<ItemFormData>) => void;
|
|
uploadedPhoto: UploadedPhoto | null;
|
|
cropBounds: CropBounds | null;
|
|
setCropBounds: (bounds: CropBounds | null) => void;
|
|
isLoading: boolean;
|
|
error: string | null;
|
|
photoError: string | null;
|
|
goToStep: (step: 'details' | 'photo' | 'preview' | 'confirm') => void;
|
|
nextStep: () => void;
|
|
prevStep: () => void;
|
|
uploadPhoto: (file: File, photoItemId?: number) => Promise<void>;
|
|
submitItem: (userId: number) => Promise<any>;
|
|
reset: () => void;
|
|
}
|
|
|
|
const initialFormData: ItemFormData = {
|
|
name: '',
|
|
category: '',
|
|
item_type: '',
|
|
quantity: 1,
|
|
barcode: '',
|
|
part_number: '',
|
|
box_label: '',
|
|
};
|
|
|
|
export function useItemCreate(): UseItemCreateReturn {
|
|
const [step, setStep] = useState<'details' | 'photo' | 'preview' | 'confirm'>('details');
|
|
const [formData, setFormDataState] = useState<ItemFormData>(initialFormData);
|
|
const [uploadedPhoto, setUploadedPhoto] = useState<UploadedPhoto | null>(null);
|
|
const [cropBounds, setCropBounds] = useState<CropBounds | null>(null);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [photoError, setPhotoError] = useState<string | null>(null);
|
|
const [itemId, setItemId] = useState<number | null>(null);
|
|
|
|
const setFormData = useCallback((data: Partial<ItemFormData>) => {
|
|
setFormDataState((prev) => ({ ...prev, ...data }));
|
|
}, []);
|
|
|
|
const goToStep = useCallback((newStep: 'details' | 'photo' | 'preview' | 'confirm') => {
|
|
setError(null);
|
|
setPhotoError(null);
|
|
setStep(newStep);
|
|
}, []);
|
|
|
|
const nextStep = useCallback(() => {
|
|
const steps: Array<'details' | 'photo' | 'preview' | 'confirm'> = ['details', 'photo', 'preview', 'confirm'];
|
|
const currentIndex = steps.indexOf(step);
|
|
if (currentIndex < steps.length - 1) {
|
|
goToStep(steps[currentIndex + 1]);
|
|
}
|
|
}, [step, goToStep]);
|
|
|
|
const prevStep = useCallback(() => {
|
|
const steps: Array<'details' | 'photo' | 'preview' | 'confirm'> = ['details', 'photo', 'preview', 'confirm'];
|
|
const currentIndex = steps.indexOf(step);
|
|
if (currentIndex > 0) {
|
|
goToStep(steps[currentIndex - 1]);
|
|
}
|
|
}, [step, goToStep]);
|
|
|
|
const uploadPhoto = useCallback(
|
|
async (file: File, photoItemId?: number) => {
|
|
const idToUse = photoItemId || itemId;
|
|
|
|
if (!idToUse) {
|
|
setPhotoError('Item must be created before uploading photo');
|
|
return;
|
|
}
|
|
|
|
setPhotoError(null);
|
|
setIsLoading(true);
|
|
|
|
try {
|
|
const formDataUpload = new FormData();
|
|
formDataUpload.append('file', file);
|
|
|
|
// If crop bounds are set, add them to the request
|
|
if (cropBounds) {
|
|
formDataUpload.append('crop_bounds', JSON.stringify(cropBounds));
|
|
}
|
|
|
|
const response = await inventoryApi.uploadItemPhoto(idToUse, formDataUpload);
|
|
|
|
if (response.status === 'ok' && response.photo) {
|
|
setUploadedPhoto({
|
|
thumbnail_url: response.photo.thumbnail_url,
|
|
full_url: response.photo.full_url,
|
|
uploaded_at: response.photo.uploaded_at,
|
|
});
|
|
setIsLoading(false);
|
|
} else {
|
|
throw new Error('Invalid response from server');
|
|
}
|
|
} catch (err: any) {
|
|
const errorMsg = err.message || 'Photo upload failed';
|
|
setPhotoError(errorMsg);
|
|
setIsLoading(false);
|
|
throw new Error(errorMsg);
|
|
}
|
|
},
|
|
[itemId, cropBounds]
|
|
);
|
|
|
|
const submitItem = useCallback(
|
|
async (userId: number) => {
|
|
setError(null);
|
|
setIsLoading(true);
|
|
|
|
try {
|
|
// Validate form data
|
|
if (!formData.name.trim()) {
|
|
const errorMsg = 'Item name is required';
|
|
setError(errorMsg);
|
|
setIsLoading(false);
|
|
return undefined;
|
|
}
|
|
if (!formData.category) {
|
|
const errorMsg = 'Category is required';
|
|
setError(errorMsg);
|
|
setIsLoading(false);
|
|
return undefined;
|
|
}
|
|
if (!formData.item_type) {
|
|
const errorMsg = 'Item type is required';
|
|
setError(errorMsg);
|
|
setIsLoading(false);
|
|
return undefined;
|
|
}
|
|
|
|
// Extract image data if provided
|
|
const { extractedImageBlob, imageProcessing, ...itemData } = formData;
|
|
|
|
// Create item first (without photo)
|
|
const createdItem = await inventoryApi.createItem(userId, itemData);
|
|
|
|
if (!createdItem.id) {
|
|
const errorMsg = 'Failed to create item';
|
|
setError(errorMsg);
|
|
setIsLoading(false);
|
|
return undefined;
|
|
}
|
|
|
|
setItemId(createdItem.id);
|
|
|
|
// AUTO-UPLOAD PHOTO if we have both extractedImageBlob and imageProcessing
|
|
if (extractedImageBlob && imageProcessing && createdItem.id) {
|
|
try {
|
|
const formDataUpload = new FormData();
|
|
formDataUpload.append('file', extractedImageBlob);
|
|
|
|
// If crop bounds are set, add them to the request
|
|
if (imageProcessing.crop_bounds) {
|
|
const cropBoundsStr = JSON.stringify(imageProcessing.crop_bounds);
|
|
formDataUpload.append('crop_bounds', cropBoundsStr);
|
|
}
|
|
|
|
await inventoryApi.uploadItemPhoto(createdItem.id, formDataUpload);
|
|
|
|
toast.success('Item created + photo saved');
|
|
} catch (photoErr) {
|
|
console.warn('Photo upload failed, but item created:', photoErr);
|
|
toast.success('Item created');
|
|
}
|
|
} else if (extractedImageBlob || imageProcessing) {
|
|
// Only one of the two is provided, so skip photo upload
|
|
toast.success('Item created');
|
|
} else {
|
|
toast.success('Item created');
|
|
}
|
|
|
|
setIsLoading(false);
|
|
|
|
return createdItem;
|
|
} catch (err: any) {
|
|
const errorMsg = err.message || 'Failed to create item';
|
|
setError(errorMsg);
|
|
setIsLoading(false);
|
|
return undefined;
|
|
}
|
|
},
|
|
[formData]
|
|
);
|
|
|
|
const reset = useCallback(() => {
|
|
setStep('details');
|
|
setFormDataState(initialFormData);
|
|
setUploadedPhoto(null);
|
|
setCropBounds(null);
|
|
setItemId(null);
|
|
setError(null);
|
|
setPhotoError(null);
|
|
}, []);
|
|
|
|
return {
|
|
step,
|
|
formData,
|
|
setFormData,
|
|
uploadedPhoto,
|
|
cropBounds,
|
|
setCropBounds,
|
|
isLoading,
|
|
error,
|
|
photoError,
|
|
goToStep,
|
|
nextStep,
|
|
prevStep,
|
|
uploadPhoto,
|
|
submitItem,
|
|
reset,
|
|
};
|
|
}
|