241 lines
8.3 KiB
TypeScript
241 lines
8.3 KiB
TypeScript
import React, { forwardRef, useState, useEffect, useMemo } from 'react';
|
|
import { View, TouchableOpacity, Alert } from 'react-native';
|
|
import { Formik } from 'formik';
|
|
import * as Yup from 'yup';
|
|
import { MyTextInput, BottomDropdown, MyText, tw, ImageUploaderNeo } from 'common-ui';
|
|
import ProductsSelector from './ProductsSelector';
|
|
import { trpc } from '../src/trpc-client';
|
|
import usePickImage from 'common-ui/src/components/use-pick-image';
|
|
import { useUploadToObjectStorage } from '../hooks/useUploadToObjectStorage';
|
|
|
|
export interface StoreFormData {
|
|
name: string;
|
|
description: string;
|
|
imageUrl?: string;
|
|
owner: number;
|
|
products: number[];
|
|
}
|
|
|
|
interface StoreImage {
|
|
uri: string;
|
|
mimeType: string;
|
|
isExisting: boolean;
|
|
}
|
|
|
|
export interface StoreFormRef {
|
|
// Add methods if needed
|
|
}
|
|
|
|
interface StoreFormProps {
|
|
mode: 'create' | 'edit';
|
|
initialValues: StoreFormData;
|
|
onSubmit: (values: StoreFormData) => void;
|
|
isLoading: boolean;
|
|
storeId?: number;
|
|
}
|
|
|
|
// Extend Formik values with images array
|
|
interface FormikStoreValues extends StoreFormData {
|
|
images: StoreImage[];
|
|
}
|
|
|
|
const validationSchema = Yup.object().shape({
|
|
name: Yup.string().required('Name is required'),
|
|
description: Yup.string(),
|
|
imageUrl: Yup.string(),
|
|
owner: Yup.number().required('Owner is required'),
|
|
products: Yup.array().of(Yup.number()),
|
|
});
|
|
|
|
const StoreForm = forwardRef<StoreFormRef, StoreFormProps>((props, ref) => {
|
|
const { mode, initialValues, onSubmit, isLoading, storeId } = props;
|
|
const { data: staffData } = trpc.admin.staffUser.getStaff.useQuery();
|
|
const { data: productsData } = trpc.admin.product.getProducts.useQuery();
|
|
|
|
// Build initial form values with images array
|
|
const buildInitialValues = (): FormikStoreValues => {
|
|
const images: StoreImage[] = [];
|
|
if (initialValues.imageUrl) {
|
|
images.push({
|
|
uri: initialValues.imageUrl,
|
|
mimeType: 'image/jpeg',
|
|
isExisting: true,
|
|
});
|
|
}
|
|
return {
|
|
...initialValues,
|
|
images,
|
|
};
|
|
};
|
|
|
|
const [formInitialValues, setFormInitialValues] = useState<FormikStoreValues>(buildInitialValues());
|
|
|
|
// For edit mode, pre-select products belonging to this store
|
|
const initialSelectedProducts = useMemo(() => {
|
|
if (mode !== 'edit' || !productsData?.products) return [];
|
|
return productsData.products
|
|
.filter(p => p.storeId === storeId)
|
|
.map(p => p.id);
|
|
}, [mode, productsData?.products, storeId]);
|
|
|
|
useEffect(() => {
|
|
setFormInitialValues({
|
|
...buildInitialValues(),
|
|
products: initialSelectedProducts,
|
|
});
|
|
}, [initialValues, initialSelectedProducts]);
|
|
|
|
const staffOptions = staffData?.staff.map(staff => ({
|
|
label: staff.name,
|
|
value: staff.id,
|
|
})) || [];
|
|
|
|
const { uploadSingle, isUploading } = useUploadToObjectStorage();
|
|
|
|
return (
|
|
<Formik
|
|
initialValues={formInitialValues}
|
|
validationSchema={validationSchema}
|
|
onSubmit={onSubmit}
|
|
enableReinitialize
|
|
>
|
|
{({ handleChange, handleSubmit, values, setFieldValue, errors, touched }) => {
|
|
// Image picker that adds to Formik field
|
|
const handleImagePick = usePickImage({
|
|
setFile: async (assets: any) => {
|
|
if (!assets || (Array.isArray(assets) && assets.length === 0)) {
|
|
return;
|
|
}
|
|
|
|
const files = Array.isArray(assets) ? assets : [assets];
|
|
const newImages: StoreImage[] = files.map((asset) => ({
|
|
uri: asset.uri,
|
|
mimeType: asset.mimeType || 'image/jpeg',
|
|
isExisting: false,
|
|
}));
|
|
|
|
// Add to Formik images field
|
|
const currentImages = values.images || [];
|
|
setFieldValue('images', [...currentImages, ...newImages]);
|
|
},
|
|
multiple: false,
|
|
});
|
|
|
|
// Remove image - works for both existing and new
|
|
const handleRemoveImage = (image: { uri: string; mimeType: string }) => {
|
|
const currentImages = values.images || [];
|
|
const removedImage = currentImages.find(img => img.uri === image.uri);
|
|
const newImages = currentImages.filter(img => img.uri !== image.uri);
|
|
|
|
setFieldValue('images', newImages);
|
|
|
|
// If we removed an existing image, also clear the imageUrl
|
|
if (removedImage?.isExisting) {
|
|
setFieldValue('imageUrl', undefined);
|
|
}
|
|
};
|
|
|
|
const submit = async () => {
|
|
try {
|
|
let imageUrl: string | undefined;
|
|
|
|
// Get new images that need to be uploaded
|
|
const newImages = values.images.filter(img => !img.isExisting);
|
|
|
|
if (newImages.length > 0) {
|
|
// Upload the first new image (single image for stores)
|
|
const image = newImages[0];
|
|
const response = await fetch(image.uri);
|
|
const imageBlob = await response.blob();
|
|
const { key } = await uploadSingle(imageBlob, image.mimeType, 'store');
|
|
imageUrl = key;
|
|
} else {
|
|
// Check if there's an existing image remaining
|
|
const existingImage = values.images.find(img => img.isExisting);
|
|
if (existingImage) {
|
|
imageUrl = existingImage.uri;
|
|
}
|
|
}
|
|
|
|
// Submit form with imageUrl (without images array)
|
|
const { images, ...submitValues } = values;
|
|
onSubmit({ ...submitValues, imageUrl });
|
|
} catch (error) {
|
|
console.error('Upload error:', error);
|
|
Alert.alert('Error', error instanceof Error ? error.message : 'Failed to upload image');
|
|
}
|
|
};
|
|
|
|
// Prepare images for ImageUploaderNeo (convert to expected format)
|
|
const imagesForUploader = (values.images || []).map(img => ({
|
|
uri: img.uri,
|
|
mimeType: img.mimeType,
|
|
}));
|
|
|
|
return (
|
|
<View>
|
|
<MyTextInput
|
|
topLabel="Store Name"
|
|
placeholder="Enter store name"
|
|
value={values.name}
|
|
onChangeText={handleChange('name')}
|
|
error={!!(touched.name && errors.name)}
|
|
style={{ marginBottom: 16 }}
|
|
/>
|
|
<MyTextInput
|
|
topLabel="Description"
|
|
placeholder="Enter store description"
|
|
multiline
|
|
numberOfLines={3}
|
|
value={values.description}
|
|
onChangeText={handleChange('description')}
|
|
error={!!(touched.description && errors.description)}
|
|
style={{ marginBottom: 16 }}
|
|
/>
|
|
<BottomDropdown
|
|
label="Owner"
|
|
value={values.owner}
|
|
options={staffOptions}
|
|
onValueChange={(value) => setFieldValue('owner', value)}
|
|
placeholder="Select owner"
|
|
error={!!(touched.owner && errors.owner)}
|
|
style={{ marginBottom: 16 }}
|
|
/>
|
|
<ProductsSelector
|
|
value={values.products || []}
|
|
onChange={(value) => setFieldValue('products', value)}
|
|
multiple={true}
|
|
label="Products"
|
|
placeholder="Select products"
|
|
isDisabled={(product) => product.storeId !== null && product.storeId !== storeId}
|
|
labelFormat={(product) => `${product.name} - ₹${product.price}`}
|
|
/>
|
|
<View style={tw`mb-6`}>
|
|
<MyText style={tw`text-sm font-bold text-gray-700 mb-3 uppercase tracking-wider`}>Store Image</MyText>
|
|
|
|
<ImageUploaderNeo
|
|
images={imagesForUploader}
|
|
onUploadImage={handleImagePick}
|
|
onRemoveImage={handleRemoveImage}
|
|
allowMultiple={false}
|
|
/>
|
|
</View>
|
|
<TouchableOpacity
|
|
onPress={submit}
|
|
disabled={isLoading || isUploading}
|
|
style={tw`px-4 py-2 rounded-lg shadow-lg items-center mt-2 ${isLoading || isUploading ? 'bg-gray-400' : 'bg-blue-500'}`}
|
|
>
|
|
<MyText style={tw`text-white text-lg font-bold`}>
|
|
{isUploading ? 'Uploading...' : isLoading ? (mode === 'create' ? 'Creating...' : 'Updating...') : (mode === 'create' ? 'Create Store' : 'Update Store')}
|
|
</MyText>
|
|
</TouchableOpacity>
|
|
</View>
|
|
);
|
|
}}
|
|
</Formik>
|
|
);
|
|
});
|
|
|
|
StoreForm.displayName = 'StoreForm';
|
|
|
|
export default StoreForm;
|