This commit is contained in:
phaichayon
2026-06-16 09:19:17 +07:00
parent b8f13e36b3
commit dff22d75b5
32 changed files with 5786 additions and 68 deletions

View File

@@ -40,7 +40,6 @@ import {
deleteQuotationFollowupMutation,
deleteQuotationItemMutation,
deleteQuotationTopicMutation,
updateQuotationMutation,
updateQuotationAttachmentMutation,
updateQuotationCustomerMutation,
updateQuotationFollowupMutation,
@@ -69,7 +68,9 @@ import type {
QuotationTopicMutationPayload,
QuotationTopicRecord
} from '../api/types';
import { submitQuotationApprovalMutation } from '@/features/foundation/approval/mutations';
import { QuotationFormSheet } from './quotation-form-sheet';
import { QuotationApprovalTab } from './quotation-approval-tab';
import { QuotationStatusBadge } from './quotation-status-badge';
function FieldItem({ label, value }: { label: string; value: string | null | undefined }) {
@@ -473,7 +474,14 @@ export function QuotationDetail({
canManageTopics,
canManageFollowups,
canManageAttachments,
canCreateRevision
canCreateRevision,
canSubmitApproval,
canApproveApproval,
canRejectApproval,
canReturnApproval,
activeBusinessRole,
isOrgAdmin,
currentUserId
}: {
quotationId: string;
referenceData: QuotationReferenceData;
@@ -484,6 +492,13 @@ export function QuotationDetail({
canManageFollowups: boolean;
canManageAttachments: boolean;
canCreateRevision: boolean;
canSubmitApproval: boolean;
canApproveApproval: boolean;
canRejectApproval: boolean;
canReturnApproval: boolean;
activeBusinessRole: string | null;
isOrgAdmin: boolean;
currentUserId: string;
}) {
const { data } = useSuspenseQuery(quotationByIdOptions(quotationId));
const { data: itemsData } = useSuspenseQuery(quotationItemsOptions(quotationId));
@@ -611,7 +626,7 @@ export function QuotationDetail({
onError: (error) => toast.error(error instanceof Error ? error.message : 'Failed to create revision')
});
const submitForApproval = useMutation({
...updateQuotationMutation,
...submitQuotationApprovalMutation,
onSuccess: () => toast.success('Quotation moved to pending approval'),
onError: (error) =>
toast.error(error instanceof Error ? error.message : 'Failed to submit for approval')
@@ -655,7 +670,8 @@ export function QuotationDetail({
const canReviseByStatus = ['accepted', 'rejected', 'sent', 'approved'].includes(
status?.code ?? ''
);
const canSubmitApproval = ['draft', 'revised'].includes(status?.code ?? '');
const canSubmitCurrentQuotation =
canSubmitApproval && ['draft', 'revised'].includes(status?.code ?? '');
return (
<div className='space-y-6'>
@@ -814,44 +830,11 @@ export function QuotationDetail({
</div>
</div>
<div className='flex flex-wrap gap-2'>
{canUpdate && canSubmitApproval ? (
{canSubmitCurrentQuotation ? (
<Button
variant='outline'
isLoading={submitForApproval.isPending}
onClick={() =>
submitForApproval.mutate({
id: quotationId,
values: {
enquiryId: quotation.enquiryId,
customerId: quotation.customerId,
contactId: quotation.contactId,
quotationDate: quotation.quotationDate,
validUntil: quotation.validUntil,
quotationType: quotation.quotationType,
projectName: quotation.projectName ?? '',
projectLocation: quotation.projectLocation ?? '',
attention: quotation.attention ?? '',
branchId: quotation.branchId,
currency: quotation.currency,
exchangeRate: quotation.exchangeRate,
status:
referenceData.statuses.find((item) => item.code === 'pending_approval')?.id ??
quotation.status,
chancePercent: quotation.chancePercent,
isHotProject: quotation.isHotProject,
competitor: quotation.competitor ?? '',
reference: quotation.reference ?? '',
notes: quotation.notes ?? '',
salesmanId: quotation.salesmanId,
discount: quotation.discount,
discountType: quotation.discountType,
taxRate: quotation.taxRate,
sentVia: quotation.sentVia,
revisionRemark: quotation.revisionRemark,
isActive: quotation.isActive
}
})
}
onClick={() => submitForApproval.mutate({ id: quotationId })}
>
<Icons.send className='mr-2 h-4 w-4' /> Submit for approval
</Button>
@@ -1191,15 +1174,17 @@ export function QuotationDetail({
</TabsContent>
<TabsContent value='approval'>
<Card>
<CardHeader>
<CardTitle>Approval Placeholder</CardTitle>
<CardDescription>Approval workflow is deferred beyond Task E.</CardDescription>
</CardHeader>
<CardContent>
<EmptyState message='Approval state and approver routing will land in a later task.' />
</CardContent>
</Card>
<QuotationApprovalTab
quotationId={quotationId}
quotationStatusCode={status?.code ?? null}
canSubmitApproval={canSubmitApproval}
canApproveApproval={canApproveApproval}
canRejectApproval={canRejectApproval}
canReturnApproval={canReturnApproval}
activeBusinessRole={activeBusinessRole}
isOrgAdmin={isOrgAdmin}
currentUserId={currentUserId}
/>
</TabsContent>
<TabsContent value='preview'>