This commit is contained in:
phaichayon
2026-06-11 12:46:57 +07:00
parent 1993d88306
commit 7a390cf0df
720 changed files with 100919 additions and 0 deletions

View File

@@ -0,0 +1,146 @@
'use client';
import { useState } from 'react';
import { useMutation, useSuspenseQuery } from '@tanstack/react-query';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { approvalsQueryOptions, crmReferenceQueryOptions } from '../api/queries';
import { approveQuotationMutation, rejectQuotationMutation } from '../api/mutations';
import { BranchBadge, SectionCard, TimelineList } from './shared';
import { formatCurrency } from '../utils/format';
function DecisionDialog({
label,
description,
isPending,
onConfirm
}: {
label: string;
description: string;
isPending: boolean;
onConfirm: (comment: string) => void;
}) {
const [comment, setComment] = useState('');
return (
<Dialog>
<DialogTrigger asChild>
<Button variant='outline' size='sm'>
{label}
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>{label}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<Input value={comment} onChange={(event) => setComment(event.target.value)} placeholder='Comment' />
<DialogFooter>
<Button isLoading={isPending} onClick={() => onConfirm(comment)}>
Confirm
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
export function ApprovalsPage() {
const { data } = useSuspenseQuery(approvalsQueryOptions({ page: 1, limit: 10 }));
const { data: reference } = useSuspenseQuery(crmReferenceQueryOptions());
const baseApproveMutation = approveQuotationMutation;
const baseRejectMutation = rejectQuotationMutation;
const approveMutation = useMutation({
...baseApproveMutation,
onSuccess: async (...args) => {
await baseApproveMutation.onSuccess?.(...args);
toast.success('Approve mock สำเร็จ');
}
});
const rejectMutation = useMutation({
...baseRejectMutation,
onSuccess: async (...args) => {
await baseRejectMutation.onSuccess?.(...args);
toast.success('Reject mock สำเร็จ');
}
});
return (
<div className='grid gap-4 xl:grid-cols-12'>
<SectionCard
title='Pending Approval List'
description='Sequential approval MVP'
className='xl:col-span-7'
>
<div className='space-y-3'>
{data.items.map((item) => {
const branch = reference.branches.find((value) => value.id === item.branchId);
return (
<div key={item.id} className='rounded-xl border bg-muted/10 p-4'>
<div className='flex flex-wrap items-start justify-between gap-3'>
<div>
<div className='flex items-center gap-2'>
<p className='font-semibold'>{item.quotationCode}</p>
{branch ? <BranchBadge branch={branch} /> : null}
</div>
<p className='text-muted-foreground mt-1 text-sm'>
{item.productType} / Submitter: {item.submitterName}
</p>
<p className='mt-2 text-sm font-medium'>
Level {item.approverLevel} / {formatCurrency(item.amount)}
</p>
</div>
<div className='flex gap-2'>
<DecisionDialog
label='Approve'
description='อนุมัติ quotation mock นี้'
isPending={approveMutation.isPending}
onConfirm={(comment) =>
approveMutation.mutate({ quotationId: item.quotationId, comment })
}
/>
<DecisionDialog
label='Reject'
description='Reject quotation mock นี้'
isPending={rejectMutation.isPending}
onConfirm={(comment) =>
rejectMutation.mutate({ quotationId: item.quotationId, comment })
}
/>
</div>
</div>
</div>
);
})}
</div>
</SectionCard>
<SectionCard
title='Approval Timeline'
description='ภาพรวมเส้นทางการอนุมัติ'
className='xl:col-span-5'
>
<TimelineList
items={data.items.map((item) => ({
id: item.id,
title: `${item.quotationCode} / Level ${item.approverLevel}`,
detail: `${item.submitterName} ส่งอนุมัติสำหรับ ${item.productType}`,
timestamp: '2026-06-11'
}))}
/>
</SectionCard>
</div>
);
}