232 lines
8.3 KiB
TypeScript
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>
|
|
)
|
|
}
|