task-d complate

This commit is contained in:
phaichayon
2026-06-15 13:46:33 +07:00
parent 8148850fda
commit 1d2406483e
32 changed files with 4812 additions and 39 deletions

View 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} />
</>
);
}