This commit is contained in:
phaichayon
2026-07-01 08:44:35 +07:00
parent 0c39472dca
commit 6d6cd3a6df
32 changed files with 1284 additions and 333 deletions

View File

@@ -7,6 +7,7 @@ import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { AuditLogTab } from '@/features/crm/components/audit-log-tab';
import { formatDateTime } from '@/lib/date-format';
import { formatNumber } from '@/lib/number-format';
import { leadByIdOptions } from '../api/queries';
@@ -111,7 +112,7 @@ export function LeadDetail({
<TabsList>
<TabsTrigger value='followups'>Follow-ups</TabsTrigger>
<TabsTrigger value='linked-opportunities'>Linked Opportunities</TabsTrigger>
<TabsTrigger value='activity'>Activity</TabsTrigger>
<TabsTrigger value='activity'>Audit Log</TabsTrigger>
</TabsList>
<TabsContent value='followups'>
@@ -152,26 +153,11 @@ export function LeadDetail({
</TabsContent>
<TabsContent value='activity'>
<Card>
<CardHeader>
<CardTitle>Activity</CardTitle>
</CardHeader>
<CardContent className='space-y-3'>
{lead.activity.length === 0 ? (
<div className='text-muted-foreground text-sm'>No activity yet.</div>
) : (
lead.activity.map((activity) => (
<div key={activity.id} className='border-border rounded-md border p-3'>
<div className='font-medium'>{activity.action}</div>
<div className='text-muted-foreground mt-1 text-xs'>
{activity.actorName ?? activity.userId} -{' '}
{formatDateTime(activity.createdAt)}
</div>
</div>
))
)}
</CardContent>
</Card>
<AuditLogTab
items={lead.activity}
emptyMessage='No audit log recorded yet.'
description='System-generated immutable history for this lead.'
/>
</TabsContent>
</Tabs>
</div>

View File

@@ -23,7 +23,15 @@ export function LeadFollowupPanel({
leadId: string;
detail: LeadDetailResponse;
}) {
const { FormTextField, FormSelectField, FormTextareaField } = useFormFields<FollowupFormValues>();
const { FormDatePickerField, FormSelectField, FormTextareaField } =
useFormFields<FollowupFormValues>();
const mutation = useMutation({
...createLeadFollowupMutation,
onError: (error) => {
toast.error(error instanceof Error ? error.message : 'Unable to save lead follow-up');
}
});
const form = useAppForm({
defaultValues: {
@@ -42,80 +50,78 @@ export function LeadFollowupPanel({
nextFollowupDate: value.nextFollowupDate || null
}
});
}
});
const mutation = useMutation({
...createLeadFollowupMutation,
onSuccess: () => {
toast.success('บันทึก follow-up สำเร็จ');
toast.success('Saved lead follow-up successfully');
form.reset();
},
onError: (error) => {
toast.error(error instanceof Error ? error.message : 'ไม่สามารถบันทึก follow-up ได้');
}
});
return (
<div className='space-y-4'>
<Card>
<CardHeader>
<CardTitle>New Follow-up</CardTitle>
</CardHeader>
<CardContent>
<form.AppForm>
<form.Form id='lead-followup-form' className='grid gap-4 md:grid-cols-2'>
<FormTextField name='followupDate' label='Follow-up Date' placeholder='YYYY-MM-DD' />
<FormTextField
name='nextFollowupDate'
label='Next Follow-up Date'
placeholder='YYYY-MM-DD'
<Card>
<CardHeader>
<CardTitle>Follow-ups</CardTitle>
</CardHeader>
<CardContent className='flex flex-col gap-6'>
<form.AppForm>
<form.Form id='lead-followup-form' className='grid gap-4 md:grid-cols-2'>
<FormDatePickerField
name='followupDate'
label='Follow-up Date'
required
placeholder='YYYY-MM-DD'
/>
<FormDatePickerField
name='nextFollowupDate'
label='Next Follow-up Date'
placeholder='YYYY-MM-DD'
/>
<FormSelectField
name='followupStatus'
label='Follow-up Status'
options={detail.referenceData.followupStatuses.map((item) => ({
value: item.id,
label: item.label
}))}
/>
<div className='md:col-span-2'>
<FormTextareaField
name='note'
label='Note'
size='md'
placeholder='Summarize the latest customer conversation and next step.'
/>
<FormSelectField
name='followupStatus'
label='Follow-up Status'
options={detail.referenceData.followupStatuses.map((item) => ({
value: item.id,
label: item.label
}))}
/>
<div />
<FormTextareaField name='note' label='Note' className='md:col-span-2' />
<div className='md:col-span-2'>
<Button type='submit' form='lead-followup-form' isLoading={mutation.isPending}>
Save Follow-up
</Button>
</div>
</form.Form>
</form.AppForm>
</CardContent>
</Card>
</div>
</form.Form>
</form.AppForm>
<Card>
<CardHeader>
<CardTitle>History</CardTitle>
</CardHeader>
<CardContent className='space-y-3'>
<div className='flex justify-end'>
<Button type='submit' form='lead-followup-form' isLoading={mutation.isPending}>
Save Follow-up
</Button>
</div>
<div className='flex flex-col gap-3'>
{detail.followups.length === 0 ? (
<div className='text-muted-foreground text-sm'>No follow-up history yet.</div>
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center text-sm'>
No follow-ups yet.
</div>
) : (
detail.followups.map((followup) => (
<div key={followup.id} className='border-border rounded-md border p-3'>
<div className='flex items-center justify-between gap-3'>
<div className='font-medium'>
{followup.followupStatusLabel ?? followup.followupStatus}
<div key={followup.id} className='rounded-lg border p-4'>
<div className='font-medium'>{followup.followupStatusLabel ?? followup.followupStatus}</div>
<div className='text-muted-foreground mt-1 text-xs'>
{formatDate(followup.followupDate)}
</div>
<div className='mt-2 text-sm'>{followup.note || '-'}</div>
{followup.nextFollowupDate ? (
<div className='text-muted-foreground mt-2 text-xs'>
Next: {formatDate(followup.nextFollowupDate)}
</div>
<div className='text-muted-foreground text-xs'>{formatDate(followup.followupDate)}</div>
</div>
{followup.note ? <div className='mt-2 text-sm'>{followup.note}</div> : null}
<div className='text-muted-foreground mt-2 text-xs'>
{followup.createdByName ?? followup.createdBy}
</div>
) : null}
</div>
))
)}
</CardContent>
</Card>
</div>
</div>
</CardContent>
</Card>
);
}

View File

@@ -258,7 +258,10 @@ async function listLeadActivity(
entityId: log.entityId,
createdAt: log.createdAt,
userId: log.userId,
actorName: actorMap.get(log.userId) ?? null
actorName: actorMap.get(log.userId) ?? null,
beforeData: log.beforeData,
afterData: log.afterData,
requestId: log.requestId
}));
}
@@ -1077,4 +1080,3 @@ export function buildLeadAccessContext(input: {
}) {
return buildCrmSecurityContext(input);
}

View File

@@ -47,15 +47,7 @@ export interface LeadLinkedOpportunitySummary {
updatedAt: string;
}
export interface LeadActivityRecord {
id: string;
action: string;
entityType: string;
entityId: string;
createdAt: string;
userId: string;
actorName: string | null;
}
export type LeadActivityRecord = CrmAuditLogRecord;
export interface LeadReferenceData {
awarenesses: LeadOption[];
@@ -225,4 +217,5 @@ export interface LeadMutationSuccessResponse {
lead?: LeadDetail;
followup?: LeadFollowupSummary;
}
import type { CrmAuditLogRecord } from '@/features/crm/audit-log/types';