freshyo/apps/admin-web/src/components/SlotForm.tsx
2026-09-12 09:06:33 +05:30

232 lines
8.3 KiB
TypeScript

import React from 'react'
import type { SlotSnippetInput } from '@packages/shared'
import { Formik, FieldArray } from 'formik'
import DateTimeInput from './DateTimeInput'
import { p as P, pInput as PInput } from 'web-components'
import { trpc } from '@/lib/trpc-client'
import ProductsSelector from './ProductsSelector'
// Snippet form row — single source in @packages/shared (complete payload)
type VendorSnippet = SlotSnippetInput
interface SlotFormProps {
onSlotAdded?: () => void
initialDeliveryTime?: Date | null
initialFreezeTime?: Date | null
initialIsActive?: boolean
slotId?: number
initialProductIds?: number[]
initialGroupIds?: number[]
}
export default function SlotForm({
onSlotAdded,
initialDeliveryTime,
initialFreezeTime,
initialIsActive = true,
slotId,
initialProductIds = [],
initialGroupIds = [],
}: SlotFormProps) {
const { data: slotData } = trpc.admin.slots.getSlotById.useQuery(
{ id: slotId! },
{ enabled: !!slotId }
)
const vendorSnippetsFromSlot = (slotData?.slot?.vendorSnippets || []).map((snippet: any) => ({
name: snippet.name || '',
groupIds: snippet.groupIds || [],
skuIds: snippet.skuIds || [],
validTill: snippet.validTill || null,
})) as VendorSnippet[]
const initialValues = {
deliveryTime: initialDeliveryTime || (slotData?.slot?.deliveryTime ? new Date(slotData.slot.deliveryTime) : null),
freezeTime: initialFreezeTime || (slotData?.slot?.freezeTime ? new Date(slotData.slot.freezeTime) : null),
selectedGroupIds: initialGroupIds.length > 0 ? initialGroupIds : (slotData?.slot?.groupIds || []),
selectedSkuIds: initialProductIds.length > 0 ? initialProductIds : (slotData?.slot?.products?.map((p: any) => p.id) || []),
vendorSnippetList: vendorSnippetsFromSlot,
}
const { mutate: createSlot, isPending: isCreating } = trpc.admin.slots.createSlot.useMutation()
const { mutate: updateSlot, isPending: isUpdating } = trpc.admin.slots.updateSlot.useMutation()
const isEditMode = !!slotId
const isPending = isCreating || isUpdating
const { data: groupsData } = trpc.admin.product.getGroups.useQuery()
const handleFormSubmit = (values: typeof initialValues) => {
if (!values.deliveryTime || !values.freezeTime) {
window.alert('Error: Please fill all fields')
return
}
if (values.freezeTime > values.deliveryTime) {
window.alert('Error: Freeze time must be before or equal to delivery time')
return
}
const slotInputData = {
deliveryTime: values.deliveryTime.toISOString(),
freezeTime: values.freezeTime.toISOString(),
isActive: initialIsActive,
groupIds: values.selectedGroupIds,
skuIds: values.selectedSkuIds,
vendorSnippets: values.vendorSnippetList.map((snippet: VendorSnippet) => ({
name: snippet.name,
skuIds: snippet.skuIds,
validTill: snippet.validTill ?? undefined,
})),
}
if (isEditMode && slotId) {
updateSlot(
{ id: slotId, ...slotInputData },
{
onSuccess: () => {
window.alert('Success: Slot updated successfully!')
onSlotAdded?.()
},
onError: (error: any) => {
console.log({msg: JSON.stringify(error.message)})
window.alert(`Error: ${error.message || 'Failed to update slot'}`)
},
}
)
} else {
createSlot(
slotInputData,
{
onSuccess: () => {
window.alert('Success: Slot created successfully!')
onSlotAdded?.()
},
onError: (error: any) => {
window.alert(`Error: ${error.message || 'Failed to create slot'}`)
},
}
)
}
}
return (
<Formik
initialValues={initialValues}
onSubmit={handleFormSubmit}
>
{({ handleSubmit, values, setFieldValue }) => {
const mappedGroups = (groupsData?.groups || []).map(group => ({
...group,
products: group.products.map(product => ({
...product,
id: product.id,
})),
}))
return (
<div className="mb-4">
<P className="text-xl font-bold mb-6 text-center block">
{isEditMode ? 'Edit Slot' : 'Create New Slot'}
</P>
<div className="mb-4">
<P className="text-lg font-semibold mb-2 block">Delivery Date & Time</P>
<DateTimeInput
testID="slot-delivery-datetime"
value={values.deliveryTime}
setValue={(value) => setFieldValue('deliveryTime', value)}
/>
</div>
<div className="mb-4">
<P className="text-lg font-semibold mb-2 block">Freeze Date & Time</P>
<DateTimeInput
testID="slot-freeze-datetime"
value={values.freezeTime}
setValue={(value) => setFieldValue('freezeTime', value)}
/>
</div>
<div className="mb-4">
<ProductsSelector
testID="slot-products-selector"
value={values.selectedSkuIds}
onChange={(newSkuIds) => setFieldValue('selectedSkuIds', newSkuIds)}
groups={mappedGroups}
selectedGroupIds={values.selectedGroupIds}
onGroupChange={(newGroupIds) => setFieldValue('selectedGroupIds', newGroupIds)}
label="Select Products"
placeholder="Select products for this slot"
/>
</div>
{/* Vendor Snippets */}
<FieldArray name="vendorSnippetList">
{({ push, remove }) => (
<div className="mb-4">
<P className="text-lg font-semibold mb-4 block">Vendor Snippets</P>
{values.vendorSnippetList.map((snippet: VendorSnippet, index: number) => (
<div key={index} className="bg-gray-50 p-4 rounded-lg mb-4">
<div className="mb-4">
<PInput
topLabel="Snippet Name *"
placeholder="Enter snippet name"
value={snippet.name}
onChange={(e) => setFieldValue(`vendorSnippetList.${index}.name`, e.target.value)}
/>
</div>
<div className="mb-4">
<ProductsSelector
value={snippet.skuIds || []}
onChange={(newSkuIds) => setFieldValue(`vendorSnippetList.${index}.skuIds`, newSkuIds)}
groups={mappedGroups.filter(group =>
values.selectedGroupIds.includes(group.id)
).map(group => ({
...group,
products: group.products.filter((prod: any) => values.selectedSkuIds.includes(prod.id))
}))}
selectedGroupIds={snippet.groupIds || []}
onGroupChange={(newGroupIds) => setFieldValue(`vendorSnippetList.${index}.groupIds`, newGroupIds)}
label="Select Products"
placeholder="Select products for snippet"
isDisabled={(sku) => !values.selectedSkuIds.includes(sku.id)}
/>
</div>
<button
type="button"
onClick={() => remove(index)}
className="bg-red-500 px-4 py-2 rounded-lg self-end text-white font-medium"
>
Remove Snippet
</button>
</div>
))}
<button
type="button"
onClick={() => push({ name: '', groupIds: [], skuIds: [], validTill: '' })}
className="bg-blue-500 px-4 py-3 rounded-lg items-center text-white font-medium w-full"
>
Add Vendor Snippet
</button>
</div>
)}
</FieldArray>
<button
type="button"
data-testid="create-slot-button"
aria-label="create-slot-button"
onClick={() => handleSubmit()}
disabled={isPending}
className={`${isPending ? 'bg-brand-100' : 'bg-brand-500'} p-3 rounded-lg items-center mt-6 pb-4 text-white text-base font-bold w-full disabled:opacity-70`}
>
{isPending ? (isEditMode ? 'Updating...' : 'Creating...') : (isEditMode ? 'Update Slot' : 'Create Slot')}
</button>
</div>
)}}
</Formik>
)
}