freshyo/apps/admin-web/src/components/StoreForm.tsx
2026-09-06 23:05:20 +05:30

197 lines
7 KiB
TypeScript

import { useState, useEffect, useMemo, useRef, type ChangeEvent } from 'react'
import { Formik } from 'formik'
import * as Yup from 'yup'
import { pInput as PInput, Dropdown, p as P, ImageUploader } from 'web-components'
import ProductsSelector from './ProductsSelector'
import { trpc } from '@/lib/trpc-client'
import { useUploadToObjectStorage } from '@/hooks/useUploadToObjectStorage'
import type { CreateStoreInput } from '@packages/shared'
// Store form values — required description + product selection on top of CreateStoreInput
export type StoreFormData = Omit<CreateStoreInput, 'description'> & {
description: string
products: number[]
}
interface StoreFormProps {
mode: 'create' | 'edit'
initialValues: StoreFormData
onSubmit: (values: StoreFormData) => void
isLoading: boolean
storeId?: number
}
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()),
})
function StoreForm({ mode, initialValues, onSubmit, isLoading, storeId }: StoreFormProps) {
const { data: staffData } = trpc.admin.staffUser.getStaff.useQuery()
const { data: productsData } = trpc.admin.product.getProducts.useQuery()
const [formInitialValues, setFormInitialValues] = useState<StoreFormData>(initialValues)
const [selectedImages, setSelectedImages] = useState<{ blob: Blob; mimeType: string }[]>([])
const [displayImages, setDisplayImages] = useState<{ uri?: string }[]>([])
const fileInputRef = useRef<HTMLInputElement>(null)
// For edit mode, pre-select SKUs belonging to this store's products
const initialSelectedProducts = useMemo(() => {
if (mode !== 'edit' || !productsData?.products) return []
return productsData.products
.filter(p => p.storeId === storeId)
.flatMap(p => (p.skus || []).map(sku => sku.id))
}, [mode, productsData?.products, storeId])
useEffect(() => {
setFormInitialValues({
...initialValues,
products: initialSelectedProducts,
})
}, [initialValues, initialSelectedProducts])
const existingImageUrls = useMemo(
() => (formInitialValues.imageUrl ? [formInitialValues.imageUrl] : []),
[formInitialValues.imageUrl]
)
const staffOptions = staffData?.staff.map((staff: { id: number; name: string }) => ({
label: staff.name,
value: staff.id,
})) || []
const { uploadSingle, isUploading } = useUploadToObjectStorage()
const handleImagePick = () => {
fileInputRef.current?.click()
}
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
const files = Array.from(e.target.files || [])
if (files.length === 0) {
setSelectedImages([])
setDisplayImages([])
return
}
const file = files[0]
setSelectedImages([{ blob: file, mimeType: file.type || 'image/jpeg' }])
setDisplayImages([{ uri: URL.createObjectURL(file) }])
e.target.value = ''
}
const handleRemoveImage = (uri: string) => {
const index = displayImages.findIndex(img => img.uri === uri)
if (index !== -1) {
const newDisplay = displayImages.filter((_, i) => i !== index)
const newFiles = selectedImages.filter((_, i) => i !== index)
setDisplayImages(newDisplay)
setSelectedImages(newFiles)
}
}
return (
<Formik
initialValues={formInitialValues}
validationSchema={validationSchema}
onSubmit={onSubmit}
enableReinitialize
>
{({ handleChange, handleSubmit, values, setFieldValue, errors, touched }) => {
const submit = async () => {
try {
let imageUrl: string | undefined
if (selectedImages.length > 0) {
const { blob, mimeType } = selectedImages[0]
const { presignedUrl } = await uploadSingle(blob, mimeType, 'store')
imageUrl = presignedUrl
}
onSubmit({ ...values, imageUrl })
} catch (error) {
console.error('Upload error:', error)
window.alert('Error: Failed to upload image')
}
}
return (
<div>
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={handleFileChange}
className="hidden"
/>
<PInput
topLabel="Store Name"
placeholder="Enter store name"
value={values.name}
onChange={handleChange('name')}
error={!!(touched.name && errors.name)}
style={{ marginBottom: 16 }}
/>
<PInput
topLabel="Description"
placeholder="Enter store description"
multiline
numberOfLines={3}
value={values.description}
onChange={handleChange('description')}
error={!!(touched.description && errors.description)}
style={{ marginBottom: 16 }}
/>
<div className="mb-4">
<P className="mb-1 text-sm text-gray-500 font-medium">Owner</P>
<Dropdown
label="Select owner"
value={values.owner}
options={staffOptions}
onValueChange={(value) => setFieldValue('owner', Number(value))}
className="mb-4"
/>
</div>
<ProductsSelector
value={values.products || []}
onChange={(value) => setFieldValue('products', value)}
multiple={true}
label="Products"
placeholder="Select products"
isDisabled={(sku) => sku.storeId !== null && sku.storeId !== storeId}
labelFormat={(sku) => `${sku.productName} - ₹${sku.price}`}
/>
<div className="mb-6">
<P className="text-sm font-bold text-gray-700 mb-3 uppercase tracking-wider block">Store Image</P>
<ImageUploader
images={displayImages}
existingImageUrls={existingImageUrls}
onAddImage={handleImagePick}
onRemoveImage={handleRemoveImage}
onRemoveExistingImage={() =>
setFormInitialValues((prev) => ({
...prev,
imageUrl: undefined,
}))
}
allowMultiple={false}
/>
</div>
<button
type="button"
onClick={submit}
disabled={isLoading || isUploading}
className={`px-4 py-2 rounded-lg shadow-lg items-center mt-2 text-white text-lg font-bold w-full disabled:opacity-70 ${isLoading || isUploading ? 'bg-gray-400' : 'bg-blue-500'}`}
>
{isUploading ? 'Uploading...' : isLoading ? (mode === 'create' ? 'Creating...' : 'Updating...') : (mode === 'create' ? 'Create Store' : 'Update Store')}
</button>
</div>
)
}}
</Formik>
)
}
export default StoreForm