197 lines
7 KiB
TypeScript
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
|