freshyo/apps/admin-ui/src/components/ProductForm.tsx
2026-03-26 17:36:36 +05:30

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;