261 lines
8.7 KiB
TypeScript
261 lines
8.7 KiB
TypeScript
import React, { useState, useEffect, useCallback, forwardRef, useImperativeHandle, useMemo } from 'react';
|
|
import { View, TouchableOpacity } from 'react-native';
|
|
import { Formik, FieldArray } from 'formik';
|
|
import * as Yup from 'yup';
|
|
import { MyTextInput, BottomDropdown, MyText, useTheme, DatePicker, tw, useFocusCallback, Checkbox, ImageUploaderNeo, ImageUploaderNeoItem, ImageUploaderNeoPayload } from 'common-ui';
|
|
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
|
import { trpc } from '../trpc-client';
|
|
|
|
interface ProductFormData {
|
|
name: string;
|
|
shortDescription: string;
|
|
longDescription: string;
|
|
unitId: number;
|
|
storeId: number;
|
|
price: string;
|
|
marketPrice: string;
|
|
isSuspended: boolean;
|
|
isFlashAvailable: boolean;
|
|
flashPrice: string;
|
|
deals: Deal[];
|
|
tagIds: number[];
|
|
productQuantity: number;
|
|
}
|
|
|
|
interface Deal {
|
|
quantity: string;
|
|
price: string;
|
|
validTill: Date | null;
|
|
}
|
|
|
|
export interface ProductFormRef {
|
|
clearImages: () => void;
|
|
}
|
|
|
|
interface ProductFormProps {
|
|
mode: 'create' | 'edit';
|
|
initialValues: ProductFormData;
|
|
onSubmit: (values: ProductFormData, images: ImageUploaderNeoPayload[], imagesToDelete: string[]) => void;
|
|
isLoading: boolean;
|
|
existingImages?: ImageUploaderNeoItem[];
|
|
existingImageKeys?: string[];
|
|
}
|
|
|
|
const unitOptions = [
|
|
{ label: 'Kg', value: 1 },
|
|
{ label: 'Litre', value: 2 },
|
|
{ label: 'Dozen', value: 3 },
|
|
{ label: 'Unit Piece', value: 4 },
|
|
];
|
|
|
|
const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
|
|
mode,
|
|
initialValues,
|
|
onSubmit,
|
|
isLoading,
|
|
existingImages = [],
|
|
existingImageKeys = [],
|
|
}, ref) => {
|
|
const { theme } = useTheme();
|
|
const [images, setImages] = useState<ImageUploaderNeoItem[]>(existingImages);
|
|
|
|
// Sync images state when existingImages prop changes (e.g., when async query data arrives)
|
|
useEffect(() => {
|
|
setImages(existingImages);
|
|
}, [existingImages]);
|
|
|
|
const { data: storesData } = trpc.common.getStoresSummary.useQuery();
|
|
const storeOptions = storesData?.stores.map(store => ({
|
|
label: store.name,
|
|
value: store.id,
|
|
})) || [];
|
|
|
|
const { data: tagsData } = trpc.admin.product.getProductTags.useQuery();
|
|
const tagOptions = tagsData?.tags.map(tag => ({
|
|
label: tag.tagName,
|
|
value: tag.id.toString(),
|
|
})) || [];
|
|
|
|
// Build signed URL -> S3 key mapping for existing images
|
|
const signedUrlToKey = useMemo(() => {
|
|
const map: Record<string, string> = {};
|
|
existingImages.forEach((img, i) => {
|
|
if (existingImageKeys[i]) {
|
|
map[img.imgUrl] = existingImageKeys[i];
|
|
}
|
|
});
|
|
return map;
|
|
}, [existingImages, existingImageKeys]);
|
|
|
|
return (
|
|
<Formik
|
|
initialValues={initialValues}
|
|
onSubmit={(values) => {
|
|
// New images have mimeType set, existing images have mimeType === null
|
|
const newImages = images.filter(img => img.mimeType !== null);
|
|
const deletedImageKeys = existingImages
|
|
.filter(existing => !images.some(current => current.imgUrl === existing.imgUrl))
|
|
.map(deleted => signedUrlToKey[deleted.imgUrl])
|
|
.filter(Boolean);
|
|
|
|
onSubmit(
|
|
values,
|
|
newImages.map(img => ({ url: img.imgUrl, mimeType: img.mimeType })),
|
|
deletedImageKeys,
|
|
);
|
|
}}
|
|
enableReinitialize
|
|
>
|
|
{({ handleChange, handleSubmit, values, setFieldValue, resetForm }) => {
|
|
const clearForm = useCallback(() => {
|
|
setImages([]);
|
|
resetForm();
|
|
}, [resetForm]);
|
|
|
|
useFocusCallback(clearForm);
|
|
|
|
useImperativeHandle(ref, () => ({
|
|
clearImages: clearForm,
|
|
}), [clearForm]);
|
|
|
|
const submit = () => handleSubmit();
|
|
|
|
return (
|
|
<View>
|
|
<MyTextInput
|
|
topLabel="Product Name"
|
|
placeholder="Enter product name"
|
|
value={values.name}
|
|
onChangeText={handleChange('name')}
|
|
style={{ marginBottom: 16 }}
|
|
/>
|
|
<MyTextInput
|
|
topLabel="Short Description"
|
|
placeholder="Enter short description"
|
|
multiline
|
|
numberOfLines={2}
|
|
value={values.shortDescription}
|
|
onChangeText={handleChange('shortDescription')}
|
|
style={{ marginBottom: 16 }}
|
|
/>
|
|
<MyTextInput
|
|
topLabel="Long Description"
|
|
placeholder="Enter detailed description"
|
|
multiline
|
|
numberOfLines={4}
|
|
value={values.longDescription}
|
|
onChangeText={handleChange('longDescription')}
|
|
style={{ marginBottom: 16 }}
|
|
/>
|
|
|
|
<ImageUploaderNeo
|
|
images={images}
|
|
onImageAdd={(payloads) => setImages(prev => [...prev, ...payloads.map(p => ({ imgUrl: p.url, mimeType: p.mimeType }))])}
|
|
onImageRemove={(payload) => setImages(prev => prev.filter(img => img.imgUrl !== payload.url))}
|
|
allowMultiple={true}
|
|
/>
|
|
|
|
<BottomDropdown
|
|
topLabel='Unit'
|
|
label="Unit"
|
|
value={values.unitId}
|
|
options={unitOptions}
|
|
onValueChange={(value) => setFieldValue('unitId', value)}
|
|
placeholder="Select unit"
|
|
style={{ marginBottom: 16 }}
|
|
/>
|
|
<MyTextInput
|
|
topLabel="Product Quantity"
|
|
placeholder="Enter product quantity"
|
|
keyboardType="numeric"
|
|
value={values.productQuantity.toString()}
|
|
onChangeText={(text) => setFieldValue('productQuantity', text)}
|
|
style={{ marginBottom: 16 }}
|
|
/>
|
|
<BottomDropdown
|
|
topLabel="Store"
|
|
label="Store"
|
|
value={values.storeId}
|
|
options={storeOptions}
|
|
onValueChange={(value) => setFieldValue('storeId', value)}
|
|
placeholder="Select store"
|
|
style={{ marginBottom: 16 }}
|
|
/>
|
|
<BottomDropdown
|
|
topLabel="Tags"
|
|
label="Tags"
|
|
value={values.tagIds.map(id => id.toString())}
|
|
options={tagOptions}
|
|
onValueChange={(value) => setFieldValue('tagIds', (value as string[]).map(id => parseInt(id)))}
|
|
multiple={true}
|
|
placeholder="Select tags"
|
|
style={{ marginBottom: 16 }}
|
|
/>
|
|
<MyTextInput
|
|
topLabel="Unit Price"
|
|
placeholder="Enter unit price"
|
|
keyboardType="numeric"
|
|
value={values.price}
|
|
onChangeText={handleChange('price')}
|
|
style={{ marginBottom: 16 }}
|
|
/>
|
|
<MyTextInput
|
|
topLabel="Market Price (Optional)"
|
|
placeholder="Enter market price"
|
|
keyboardType="numeric"
|
|
value={values.marketPrice}
|
|
onChangeText={handleChange('marketPrice')}
|
|
style={{ marginBottom: 16 }}
|
|
/>
|
|
|
|
<View style={tw`flex-row items-center mb-4`}>
|
|
<Checkbox
|
|
checked={values.isSuspended}
|
|
onPress={() => setFieldValue('isSuspended', !values.isSuspended)}
|
|
style={tw`mr-3`}
|
|
/>
|
|
<MyText style={tw`text-gray-700 font-medium`}>Suspend Product</MyText>
|
|
</View>
|
|
|
|
<View style={tw`flex-row items-center mb-4`}>
|
|
<Checkbox
|
|
checked={values.isFlashAvailable}
|
|
onPress={() => {
|
|
setFieldValue('isFlashAvailable', !values.isFlashAvailable);
|
|
if (values.isFlashAvailable) setFieldValue('flashPrice', '');
|
|
}}
|
|
style={tw`mr-3`}
|
|
/>
|
|
<MyText style={tw`text-gray-700 font-medium`}>Flash Available</MyText>
|
|
</View>
|
|
|
|
{values.isFlashAvailable && (
|
|
<MyTextInput
|
|
topLabel="Flash Price"
|
|
placeholder="Enter flash price"
|
|
keyboardType="numeric"
|
|
value={values.flashPrice}
|
|
onChangeText={handleChange('flashPrice')}
|
|
style={{ marginBottom: 16 }}
|
|
/>
|
|
)}
|
|
|
|
<TouchableOpacity
|
|
onPress={submit}
|
|
disabled={isLoading}
|
|
style={tw`px-4 py-2 rounded-lg shadow-lg items-center mt-2 ${isLoading ? 'bg-gray-400' : 'bg-blue-500'}`}
|
|
>
|
|
<MyText style={tw`text-white text-lg font-bold`}>
|
|
{isLoading ? (mode === 'create' ? 'Creating...' : 'Updating...') : (mode === 'create' ? 'Create Product' : 'Update Product')}
|
|
</MyText>
|
|
</TouchableOpacity>
|
|
</View>
|
|
);
|
|
}}
|
|
</Formik>
|
|
);
|
|
});
|
|
|
|
ProductForm.displayName = 'ProductForm';
|
|
|
|
export default ProductForm;
|