task-d complate
This commit is contained in:
377
src/features/crm/enquiries/components/enquiry-form-sheet.tsx
Normal file
377
src/features/crm/enquiries/components/enquiry-form-sheet.tsx
Normal file
@@ -0,0 +1,377 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { toast } from 'sonner';
|
||||
import { Icons } from '@/components/icons';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from '@/components/ui/select';
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetTitle
|
||||
} from '@/components/ui/sheet';
|
||||
import { useAppForm, useFormFields } from '@/components/ui/tanstack-form';
|
||||
import { createEnquiryMutation, updateEnquiryMutation } from '../api/mutations';
|
||||
import type { EnquiryMutationPayload, EnquiryRecord, EnquiryReferenceData } from '../api/types';
|
||||
import { enquirySchema, type EnquiryFormValues } from '../schemas/enquiry.schema';
|
||||
|
||||
function toDefaultValues(
|
||||
enquiry: EnquiryRecord | undefined,
|
||||
referenceData: EnquiryReferenceData
|
||||
): EnquiryFormValues {
|
||||
return {
|
||||
customerId: enquiry?.customerId ?? referenceData.customers[0]?.id ?? '',
|
||||
contactId: enquiry?.contactId ?? undefined,
|
||||
title: enquiry?.title ?? '',
|
||||
description: enquiry?.description ?? '',
|
||||
requirement: enquiry?.requirement ?? '',
|
||||
projectName: enquiry?.projectName ?? '',
|
||||
projectLocation: enquiry?.projectLocation ?? '',
|
||||
branchId: enquiry?.branchId ?? undefined,
|
||||
productType: enquiry?.productType ?? referenceData.productTypes[0]?.id ?? '',
|
||||
status: enquiry?.status ?? referenceData.statuses[0]?.id ?? '',
|
||||
priority:
|
||||
enquiry?.priority ?? referenceData.priorities[1]?.id ?? referenceData.priorities[0]?.id ?? '',
|
||||
leadChannel: enquiry?.leadChannel ?? undefined,
|
||||
estimatedValue: enquiry?.estimatedValue ? String(enquiry.estimatedValue) : '',
|
||||
chancePercent:
|
||||
enquiry?.chancePercent !== null && enquiry?.chancePercent !== undefined
|
||||
? String(enquiry.chancePercent)
|
||||
: '',
|
||||
expectedCloseDate: enquiry?.expectedCloseDate ? enquiry.expectedCloseDate.slice(0, 10) : '',
|
||||
competitor: enquiry?.competitor ?? '',
|
||||
source: enquiry?.source ?? '',
|
||||
isHotProject: enquiry?.isHotProject ?? false,
|
||||
notes: enquiry?.notes ?? '',
|
||||
isActive: enquiry?.isActive ?? true
|
||||
};
|
||||
}
|
||||
|
||||
export function EnquiryFormSheet({
|
||||
enquiry,
|
||||
open,
|
||||
onOpenChange,
|
||||
referenceData
|
||||
}: {
|
||||
enquiry?: EnquiryRecord;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
referenceData: EnquiryReferenceData;
|
||||
}) {
|
||||
const isEdit = !!enquiry;
|
||||
const defaultValues = useMemo(
|
||||
() => toDefaultValues(enquiry, referenceData),
|
||||
[enquiry, referenceData]
|
||||
);
|
||||
const [selectedCustomerId, setSelectedCustomerId] = useState(defaultValues.customerId);
|
||||
const { FormTextField, FormTextareaField, FormSelectField, FormSwitchField } =
|
||||
useFormFields<EnquiryFormValues>();
|
||||
|
||||
const createMutation = useMutation({
|
||||
...createEnquiryMutation,
|
||||
onSuccess: () => {
|
||||
toast.success('Enquiry created successfully');
|
||||
onOpenChange(false);
|
||||
},
|
||||
onError: (error) =>
|
||||
toast.error(error instanceof Error ? error.message : 'Failed to create enquiry')
|
||||
});
|
||||
|
||||
const updateMutation = useMutation({
|
||||
...updateEnquiryMutation,
|
||||
onSuccess: () => {
|
||||
toast.success('Enquiry updated successfully');
|
||||
onOpenChange(false);
|
||||
},
|
||||
onError: (error) =>
|
||||
toast.error(error instanceof Error ? error.message : 'Failed to update enquiry')
|
||||
});
|
||||
|
||||
const contactsForCustomer = referenceData.contacts.filter(
|
||||
(contact) => contact.customerId === selectedCustomerId
|
||||
);
|
||||
|
||||
const form = useAppForm({
|
||||
defaultValues,
|
||||
validators: {
|
||||
onSubmit: enquirySchema
|
||||
},
|
||||
onSubmit: async ({ value }) => {
|
||||
const payload: EnquiryMutationPayload = {
|
||||
customerId: value.customerId,
|
||||
contactId: value.contactId || null,
|
||||
title: value.title,
|
||||
description: value.description,
|
||||
requirement: value.requirement,
|
||||
projectName: value.projectName,
|
||||
projectLocation: value.projectLocation,
|
||||
branchId: value.branchId || null,
|
||||
productType: value.productType,
|
||||
status: value.status,
|
||||
priority: value.priority,
|
||||
leadChannel: value.leadChannel || null,
|
||||
estimatedValue: value.estimatedValue ? Number(value.estimatedValue) : null,
|
||||
chancePercent: value.chancePercent ? Number(value.chancePercent) : null,
|
||||
expectedCloseDate: value.expectedCloseDate || null,
|
||||
competitor: value.competitor,
|
||||
source: value.source,
|
||||
isHotProject: value.isHotProject ?? false,
|
||||
notes: value.notes,
|
||||
isActive: value.isActive ?? true
|
||||
};
|
||||
|
||||
if (isEdit && enquiry) {
|
||||
await updateMutation.mutateAsync({ id: enquiry.id, values: payload });
|
||||
return;
|
||||
}
|
||||
|
||||
await createMutation.mutateAsync(payload);
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedCustomerId(defaultValues.customerId);
|
||||
form.reset(defaultValues);
|
||||
}, [defaultValues, form, open]);
|
||||
|
||||
const isPending = createMutation.isPending || updateMutation.isPending;
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||
<SheetContent className='flex flex-col sm:max-w-4xl'>
|
||||
<SheetHeader>
|
||||
<SheetTitle>{isEdit ? 'Edit Enquiry' : 'New Enquiry'}</SheetTitle>
|
||||
<SheetDescription>
|
||||
Capture the opportunity details before quotation and approval stages begin.
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
|
||||
<div className='flex-1 overflow-auto'>
|
||||
<form.AppForm>
|
||||
<form.Form id='enquiry-form-sheet' className='grid gap-4 md:grid-cols-2'>
|
||||
<form.AppField
|
||||
name='customerId'
|
||||
children={(field) => (
|
||||
<field.FieldSet>
|
||||
<field.Field>
|
||||
<field.FieldLabel>Customer *</field.FieldLabel>
|
||||
<Select
|
||||
value={field.state.value ?? ''}
|
||||
onValueChange={(value) => {
|
||||
field.handleChange(value);
|
||||
field.handleBlur();
|
||||
setSelectedCustomerId(value);
|
||||
form.setFieldValue('contactId', '');
|
||||
}}
|
||||
>
|
||||
<SelectTrigger aria-label='Customer'>
|
||||
<SelectValue placeholder='Select customer' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{referenceData.customers.map((customer) => (
|
||||
<SelectItem key={customer.id} value={customer.id}>
|
||||
{customer.name} ({customer.code})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</field.Field>
|
||||
<field.FieldError />
|
||||
</field.FieldSet>
|
||||
)}
|
||||
/>
|
||||
|
||||
<form.AppField
|
||||
name='contactId'
|
||||
children={(field) => (
|
||||
<field.FieldSet>
|
||||
<field.Field>
|
||||
<field.FieldLabel>Contact</field.FieldLabel>
|
||||
<Select
|
||||
value={field.state.value ?? ''}
|
||||
onValueChange={(value) => {
|
||||
field.handleChange(value === '__none__' ? '' : value);
|
||||
field.handleBlur();
|
||||
}}
|
||||
>
|
||||
<SelectTrigger aria-label='Contact'>
|
||||
<SelectValue placeholder='Select contact' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{contactsForCustomer.length ? (
|
||||
contactsForCustomer.map((contact) => (
|
||||
<SelectItem key={contact.id} value={contact.id}>
|
||||
{contact.name}
|
||||
{contact.isPrimary ? ' (Primary)' : ''}
|
||||
</SelectItem>
|
||||
))
|
||||
) : (
|
||||
<SelectItem value='__none__'>No contact selected</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</field.Field>
|
||||
<field.FieldError />
|
||||
</field.FieldSet>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormTextField
|
||||
name='title'
|
||||
label='Title'
|
||||
required
|
||||
placeholder='Warehouse crane upgrade opportunity'
|
||||
/>
|
||||
<FormTextField
|
||||
name='projectName'
|
||||
label='Project Name'
|
||||
placeholder='Bangna Warehouse Expansion'
|
||||
/>
|
||||
<FormTextField
|
||||
name='projectLocation'
|
||||
label='Project Location'
|
||||
placeholder='Bangkok'
|
||||
/>
|
||||
<FormTextField
|
||||
name='source'
|
||||
label='Source'
|
||||
placeholder='Existing customer referral'
|
||||
/>
|
||||
<FormSelectField
|
||||
name='branchId'
|
||||
label='Branch'
|
||||
options={referenceData.branches.map((branch) => ({
|
||||
value: branch.id,
|
||||
label: branch.name
|
||||
}))}
|
||||
/>
|
||||
<FormSelectField
|
||||
name='productType'
|
||||
label='Product Type'
|
||||
required
|
||||
options={referenceData.productTypes.map((item) => ({
|
||||
value: item.id,
|
||||
label: item.label
|
||||
}))}
|
||||
/>
|
||||
<FormSelectField
|
||||
name='status'
|
||||
label='Status'
|
||||
required
|
||||
options={referenceData.statuses.map((item) => ({
|
||||
value: item.id,
|
||||
label: item.label
|
||||
}))}
|
||||
/>
|
||||
<FormSelectField
|
||||
name='priority'
|
||||
label='Priority'
|
||||
required
|
||||
options={referenceData.priorities.map((item) => ({
|
||||
value: item.id,
|
||||
label: item.label
|
||||
}))}
|
||||
/>
|
||||
<FormSelectField
|
||||
name='leadChannel'
|
||||
label='Lead Channel'
|
||||
options={referenceData.leadChannels.map((item) => ({
|
||||
value: item.id,
|
||||
label: item.label
|
||||
}))}
|
||||
/>
|
||||
<FormTextField
|
||||
name='estimatedValue'
|
||||
label='Estimated Value'
|
||||
type='number'
|
||||
placeholder='2500000'
|
||||
/>
|
||||
<FormTextField name='chancePercent' label='Chance %' type='number' placeholder='60' />
|
||||
<FormTextField
|
||||
name='expectedCloseDate'
|
||||
label='Expected Close Date'
|
||||
placeholder='YYYY-MM-DD'
|
||||
/>
|
||||
<FormTextField name='competitor' label='Competitor' placeholder='Other vendor name' />
|
||||
<div className='md:col-span-2'>
|
||||
<FormTextareaField
|
||||
name='description'
|
||||
label='Description'
|
||||
placeholder='High-level summary of the opportunity'
|
||||
/>
|
||||
</div>
|
||||
<div className='md:col-span-2'>
|
||||
<FormTextareaField
|
||||
name='requirement'
|
||||
label='Requirement'
|
||||
placeholder='Specific technical or commercial requirements'
|
||||
/>
|
||||
</div>
|
||||
<div className='md:col-span-2'>
|
||||
<FormTextareaField
|
||||
name='notes'
|
||||
label='Notes'
|
||||
placeholder='Internal notes, blockers, or guidance for the sales team'
|
||||
/>
|
||||
</div>
|
||||
<div className='md:col-span-2 grid gap-4 md:grid-cols-2'>
|
||||
<FormSwitchField
|
||||
name='isHotProject'
|
||||
label='Hot Project'
|
||||
description='Use for urgent or highly strategic opportunities.'
|
||||
/>
|
||||
<FormSwitchField
|
||||
name='isActive'
|
||||
label='Active Record'
|
||||
description='Inactive enquiries stay in history but should not progress further.'
|
||||
/>
|
||||
</div>
|
||||
</form.Form>
|
||||
</form.AppForm>
|
||||
</div>
|
||||
|
||||
<SheetFooter>
|
||||
<Button type='button' variant='outline' onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type='submit' form='enquiry-form-sheet' isLoading={isPending}>
|
||||
<Icons.check className='mr-2 h-4 w-4' />
|
||||
{isEdit ? 'Update Enquiry' : 'Create Enquiry'}
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
|
||||
export function EnquiryFormSheetTrigger({
|
||||
referenceData
|
||||
}: {
|
||||
referenceData: EnquiryReferenceData;
|
||||
}) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={() => setOpen(true)}>
|
||||
<Icons.add className='mr-2 h-4 w-4' /> Add Enquiry
|
||||
</Button>
|
||||
<EnquiryFormSheet open={open} onOpenChange={setOpen} referenceData={referenceData} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user