task-f
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useMutation, useQuery, useSuspenseQuery } from '@tanstack/react-query';
|
||||
import { toast } from 'sonner';
|
||||
import { Icons } from '@/components/icons';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import {
|
||||
approvalByIdOptions,
|
||||
approvalsQueryOptions
|
||||
} from '@/features/foundation/approval/queries';
|
||||
import { submitQuotationApprovalMutation } from '@/features/foundation/approval/mutations';
|
||||
import { ApprovalRequestPanel } from '@/features/foundation/approval/components/approval-request-panel';
|
||||
import { ApprovalStatusBadge } from '@/features/foundation/approval/components/approval-status-badge';
|
||||
|
||||
export function QuotationApprovalTab({
|
||||
quotationId,
|
||||
quotationStatusCode,
|
||||
canSubmitApproval,
|
||||
canApproveApproval,
|
||||
canRejectApproval,
|
||||
canReturnApproval,
|
||||
activeBusinessRole,
|
||||
isOrgAdmin,
|
||||
currentUserId
|
||||
}: {
|
||||
quotationId: string;
|
||||
quotationStatusCode: string | null;
|
||||
canSubmitApproval: boolean;
|
||||
canApproveApproval: boolean;
|
||||
canRejectApproval: boolean;
|
||||
canReturnApproval: boolean;
|
||||
activeBusinessRole: string | null;
|
||||
isOrgAdmin: boolean;
|
||||
currentUserId: string;
|
||||
}) {
|
||||
const [remark, setRemark] = useState('');
|
||||
const { data: requestsData } = useSuspenseQuery(
|
||||
approvalsQueryOptions({
|
||||
entityType: 'quotation',
|
||||
entityId: quotationId,
|
||||
limit: 20
|
||||
})
|
||||
);
|
||||
const latestRequest = requestsData.items[0] ?? null;
|
||||
const { data: approvalDetailData } = useQuery({
|
||||
...approvalByIdOptions(latestRequest?.id ?? ''),
|
||||
enabled: !!latestRequest
|
||||
});
|
||||
|
||||
const submitApproval = useMutation({
|
||||
...submitQuotationApprovalMutation,
|
||||
onSuccess: () => {
|
||||
toast.success('Quotation submitted for approval');
|
||||
setRemark('');
|
||||
},
|
||||
onError: (error) =>
|
||||
toast.error(error instanceof Error ? error.message : 'Unable to submit quotation')
|
||||
});
|
||||
const canSubmitNow = canSubmitApproval && ['draft', 'revised'].includes(quotationStatusCode ?? '');
|
||||
|
||||
return (
|
||||
<div className='space-y-6'>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Current Status</CardTitle>
|
||||
<CardDescription>Submission readiness and latest approval request state.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className='space-y-4'>
|
||||
<div className='flex flex-wrap items-center gap-3'>
|
||||
<ApprovalStatusBadge status={latestRequest?.status ?? quotationStatusCode ?? 'draft'} />
|
||||
<span className='text-muted-foreground text-sm'>
|
||||
{latestRequest
|
||||
? `Latest request: ${latestRequest.workflowName}`
|
||||
: 'No approval request has been submitted yet.'}
|
||||
</span>
|
||||
</div>
|
||||
{canSubmitNow ? (
|
||||
<div className='space-y-3 rounded-lg border p-4'>
|
||||
<div className='text-sm font-medium'>Submit quotation into approval flow</div>
|
||||
<Textarea
|
||||
value={remark}
|
||||
onChange={(event) => setRemark(event.target.value)}
|
||||
placeholder='Optional remark for approvers'
|
||||
rows={3}
|
||||
/>
|
||||
<Button
|
||||
isLoading={submitApproval.isPending}
|
||||
onClick={() => submitApproval.mutate({ id: quotationId, remark })}
|
||||
>
|
||||
<Icons.send className='mr-2 h-4 w-4' />
|
||||
Submit for Approval
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{approvalDetailData?.approval ? (
|
||||
<ApprovalRequestPanel
|
||||
approval={approvalDetailData.approval}
|
||||
canApprove={canApproveApproval}
|
||||
canReject={canRejectApproval}
|
||||
canReturn={canReturnApproval}
|
||||
canCancel={canSubmitApproval}
|
||||
activeBusinessRole={activeBusinessRole}
|
||||
isOrgAdmin={isOrgAdmin}
|
||||
currentUserId={currentUserId}
|
||||
/>
|
||||
) : (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Approval Timeline</CardTitle>
|
||||
<CardDescription>Submission and approver history will appear here.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center text-sm'>
|
||||
No approval activity recorded yet.
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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'>
|
||||
|
||||
Reference in New Issue
Block a user