Files
tfm_ainventory/frontend/hooks/useItemCreate.ts

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.warning('Item created (photo upload skipped)');
}
} 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,
};
}