task-fixdatetime
This commit is contained in:
@@ -4,6 +4,7 @@ import Link from 'next/link';
|
||||
import type { Column, ColumnDef } from '@tanstack/react-table';
|
||||
import { DataTableColumnHeader } from '@/components/ui/table/data-table-column-header';
|
||||
import { Icons } from '@/components/icons';
|
||||
import { formatDateTime } from '@/lib/date-format';
|
||||
import type { ApprovalListItem } from '../types';
|
||||
import { ApprovalStatusBadge } from './approval-status-badge';
|
||||
|
||||
@@ -17,7 +18,10 @@ export function getApprovalColumns(): ColumnDef<ApprovalListItem>[] {
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className='flex flex-col'>
|
||||
<Link href={`/dashboard/crm/approvals/${row.original.id}`} className='font-medium hover:underline'>
|
||||
<Link
|
||||
href={`/dashboard/crm/approvals/${row.original.id}`}
|
||||
className='font-medium hover:underline'
|
||||
>
|
||||
{row.original.entityCode ?? row.original.entityId}
|
||||
</Link>
|
||||
<span className='text-muted-foreground text-xs'>
|
||||
@@ -97,7 +101,7 @@ export function getApprovalColumns(): ColumnDef<ApprovalListItem>[] {
|
||||
header: ({ column }: { column: Column<ApprovalListItem, unknown> }) => (
|
||||
<DataTableColumnHeader column={column} title='วันที่ส่งอนุมัติ' />
|
||||
),
|
||||
cell: ({ row }) => new Date(row.original.requestedAt).toLocaleString()
|
||||
cell: ({ row }) => formatDateTime(row.original.requestedAt)
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { formatDateTime } from '@/lib/date-format';
|
||||
import {
|
||||
approveApprovalMutation,
|
||||
cancelApprovalMutation,
|
||||
@@ -52,8 +53,7 @@ export function ApprovalRequestPanel({
|
||||
const [remark, setRemark] = useState('');
|
||||
const isPending = approval.request.status === 'pending';
|
||||
const canHandleCurrentStep =
|
||||
!!approval.currentStep &&
|
||||
(isOrgAdmin || activeBusinessRole === approval.currentStep.roleCode);
|
||||
!!approval.currentStep && (isOrgAdmin || activeBusinessRole === approval.currentStep.roleCode);
|
||||
const canCancelCurrentRequest =
|
||||
canCancel && isPending && (isOrgAdmin || approval.request.requestedBy === currentUserId);
|
||||
|
||||
@@ -63,8 +63,7 @@ export function ApprovalRequestPanel({
|
||||
toast.success('อนุมัติขั้นตอนนี้สำเร็จ');
|
||||
setRemark('');
|
||||
},
|
||||
onError: (error) =>
|
||||
toast.error(error instanceof Error ? error.message : 'ไม่สามารถอนุมัติคำขอได้')
|
||||
onError: (error) => toast.error(error instanceof Error ? error.message : 'ไม่สามารถอนุมัติคำขอได้')
|
||||
});
|
||||
const rejectAction = useMutation({
|
||||
...rejectApprovalMutation,
|
||||
@@ -72,8 +71,7 @@ export function ApprovalRequestPanel({
|
||||
toast.success('ไม่อนุมัติคำขอสำเร็จ');
|
||||
setRemark('');
|
||||
},
|
||||
onError: (error) =>
|
||||
toast.error(error instanceof Error ? error.message : 'ไม่สามารถไม่อนุมัติคำขอได้')
|
||||
onError: (error) => toast.error(error instanceof Error ? error.message : 'ไม่สามารถไม่อนุมัติคำขอได้')
|
||||
});
|
||||
const returnAction = useMutation({
|
||||
...returnApprovalMutation,
|
||||
@@ -81,14 +79,12 @@ export function ApprovalRequestPanel({
|
||||
toast.success('ตีกลับคำขอเพื่อแก้ไขสำเร็จ');
|
||||
setRemark('');
|
||||
},
|
||||
onError: (error) =>
|
||||
toast.error(error instanceof Error ? error.message : 'ไม่สามารถตีกลับคำขอได้')
|
||||
onError: (error) => toast.error(error instanceof Error ? error.message : 'ไม่สามารถตีกลับคำขอได้')
|
||||
});
|
||||
const cancelAction = useMutation({
|
||||
...cancelApprovalMutation,
|
||||
onSuccess: () => toast.success('ยกเลิกคำขออนุมัติสำเร็จ'),
|
||||
onError: (error) =>
|
||||
toast.error(error instanceof Error ? error.message : 'ไม่สามารถยกเลิกคำขอได้')
|
||||
onError: (error) => toast.error(error instanceof Error ? error.message : 'ไม่สามารถยกเลิกคำขอได้')
|
||||
});
|
||||
const isActing =
|
||||
approveAction.isPending ||
|
||||
@@ -109,12 +105,15 @@ export function ApprovalRequestPanel({
|
||||
<div className='text-muted-foreground text-xs'>สถานะ</div>
|
||||
<ApprovalStatusBadge status={approval.request.status} />
|
||||
</div>
|
||||
<FieldItem label='ประเภทเอกสาร' value={approval.request.entityType.replaceAll('_', ' ')} />
|
||||
<FieldItem
|
||||
label='ประเภทเอกสาร'
|
||||
value={approval.request.entityType.replaceAll('_', ' ')}
|
||||
/>
|
||||
<FieldItem label='รหัสเอกสาร' value={approval.entityCode ?? approval.request.entityId} />
|
||||
<FieldItem label='ชื่อเอกสาร' value={approval.entityTitle} />
|
||||
<FieldItem
|
||||
label='วันที่ส่งอนุมัติ'
|
||||
value={new Date(approval.request.requestedAt).toLocaleString()}
|
||||
value={formatDateTime(approval.request.requestedAt)}
|
||||
/>
|
||||
<FieldItem label='ผู้ส่งอนุมัติ' value={approval.request.requestedBy} />
|
||||
<FieldItem
|
||||
@@ -148,7 +147,9 @@ export function ApprovalRequestPanel({
|
||||
<CardContent className='space-y-3'>
|
||||
{approval.steps.map((step) => {
|
||||
const isCurrent = approval.currentStep?.id === step.id && isPending;
|
||||
const isCompleted = approval.request.currentStep > step.stepNumber || approval.request.status === 'approved';
|
||||
const isCompleted =
|
||||
approval.request.currentStep > step.stepNumber ||
|
||||
approval.request.status === 'approved';
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -173,9 +174,7 @@ export function ApprovalRequestPanel({
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>การดำเนินการ</CardTitle>
|
||||
<CardDescription>
|
||||
ผู้อนุมัติสามารถอนุมัติ ไม่อนุมัติ หรือตีกลับในขั้นที่รับผิดชอบได้
|
||||
</CardDescription>
|
||||
<CardDescription>ผู้อนุมัติสามารถอนุมัติ ไม่อนุมัติ หรือตีกลับในขั้นที่รับผิดชอบได้</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className='space-y-4'>
|
||||
<Textarea
|
||||
@@ -200,9 +199,7 @@ export function ApprovalRequestPanel({
|
||||
<Button
|
||||
variant='destructive'
|
||||
isLoading={rejectAction.isPending}
|
||||
onClick={() =>
|
||||
rejectAction.mutate({ id: approval.request.id, values: { remark } })
|
||||
}
|
||||
onClick={() => rejectAction.mutate({ id: approval.request.id, values: { remark } })}
|
||||
>
|
||||
<Icons.warning className='mr-2 h-4 w-4' />
|
||||
ไม่อนุมัติ
|
||||
@@ -212,9 +209,7 @@ export function ApprovalRequestPanel({
|
||||
<Button
|
||||
variant='outline'
|
||||
isLoading={returnAction.isPending}
|
||||
onClick={() =>
|
||||
returnAction.mutate({ id: approval.request.id, values: { remark } })
|
||||
}
|
||||
onClick={() => returnAction.mutate({ id: approval.request.id, values: { remark } })}
|
||||
>
|
||||
<Icons.chevronLeft className='mr-2 h-4 w-4' />
|
||||
ตีกลับ
|
||||
@@ -234,7 +229,10 @@ export function ApprovalRequestPanel({
|
||||
{!canHandleCurrentStep && isPending ? (
|
||||
<div className='text-muted-foreground text-sm'>
|
||||
ขั้นตอนปัจจุบันถูกมอบหมายให้{' '}
|
||||
<span className='font-medium'>{approval.currentStep?.roleName ?? 'another role'}</span>.
|
||||
<span className='font-medium'>
|
||||
{approval.currentStep?.roleName ?? 'another role'}
|
||||
</span>
|
||||
.
|
||||
</div>
|
||||
) : null}
|
||||
{!isPending ? (
|
||||
@@ -268,16 +266,10 @@ export function ApprovalRequestPanel({
|
||||
<Badge variant='outline' className='capitalize'>
|
||||
{item.action}
|
||||
</Badge>
|
||||
<span className='text-sm font-medium'>
|
||||
{item.actorName ?? item.actedBy}
|
||||
</span>
|
||||
<span className='text-muted-foreground text-xs'>
|
||||
ขั้น {item.stepNumber}
|
||||
</span>
|
||||
</div>
|
||||
<div className='text-muted-foreground text-xs'>
|
||||
{new Date(item.actedAt).toLocaleString()}
|
||||
<span className='text-sm font-medium'>{item.actorName ?? item.actedBy}</span>
|
||||
<span className='text-muted-foreground text-xs'>ขั้น {item.stepNumber}</span>
|
||||
</div>
|
||||
<div className='text-muted-foreground text-xs'>{formatDateTime(item.actedAt)}</div>
|
||||
{item.remark ? <div className='text-sm'>{item.remark}</div> : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from '@/components/ui/dialog';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { formatDateTime } from '@/lib/date-format';
|
||||
import {
|
||||
createDocumentSequenceMutation,
|
||||
deleteDocumentSequenceMutation,
|
||||
@@ -111,25 +112,62 @@ function SequenceDialog({
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{sequence ? 'Edit Sequence' : 'Create Sequence'}</DialogTitle>
|
||||
<DialogDescription>Preview never increments the counter. Generation remains server-side only.</DialogDescription>
|
||||
<DialogDescription>
|
||||
Preview never increments the counter. Generation remains server-side only.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form onSubmit={onSubmit} className='grid gap-4'>
|
||||
<Input placeholder='Document type' value={state.documentType} onChange={(e) => setState((current) => ({ ...current, documentType: e.target.value }))} />
|
||||
<Input
|
||||
placeholder='Document type'
|
||||
value={state.documentType}
|
||||
onChange={(e) => setState((current) => ({ ...current, documentType: e.target.value }))}
|
||||
/>
|
||||
<div className='grid gap-4 md:grid-cols-2'>
|
||||
<Input placeholder='Prefix' value={state.prefix} onChange={(e) => setState((current) => ({ ...current, prefix: e.target.value }))} />
|
||||
<Input placeholder='Period' value={state.period} onChange={(e) => setState((current) => ({ ...current, period: e.target.value }))} />
|
||||
<Input
|
||||
placeholder='Prefix'
|
||||
value={state.prefix}
|
||||
onChange={(e) => setState((current) => ({ ...current, prefix: e.target.value }))}
|
||||
/>
|
||||
<Input
|
||||
placeholder='Period'
|
||||
value={state.period}
|
||||
onChange={(e) => setState((current) => ({ ...current, period: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div className='grid gap-4 md:grid-cols-3'>
|
||||
<Input placeholder='Branch ID (optional)' value={state.branchId} onChange={(e) => setState((current) => ({ ...current, branchId: e.target.value }))} />
|
||||
<Input placeholder='Current number' value={state.currentNumber} onChange={(e) => setState((current) => ({ ...current, currentNumber: e.target.value }))} />
|
||||
<Input placeholder='Padding length' value={state.paddingLength} onChange={(e) => setState((current) => ({ ...current, paddingLength: e.target.value }))} />
|
||||
<Input
|
||||
placeholder='Branch ID (optional)'
|
||||
value={state.branchId}
|
||||
onChange={(e) => setState((current) => ({ ...current, branchId: e.target.value }))}
|
||||
/>
|
||||
<Input
|
||||
placeholder='Current number'
|
||||
value={state.currentNumber}
|
||||
onChange={(e) =>
|
||||
setState((current) => ({ ...current, currentNumber: e.target.value }))
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
placeholder='Padding length'
|
||||
value={state.paddingLength}
|
||||
onChange={(e) =>
|
||||
setState((current) => ({ ...current, paddingLength: e.target.value }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex items-center justify-between rounded-lg border px-4 py-3'>
|
||||
<div>
|
||||
<div className='font-medium'>Active</div>
|
||||
<div className='text-muted-foreground text-sm'>Inactive sequences can stay preserved for legacy numbering.</div>
|
||||
<div className='text-muted-foreground text-sm'>
|
||||
Inactive sequences can stay preserved for legacy numbering.
|
||||
</div>
|
||||
</div>
|
||||
<Switch checked={state.isActive} onCheckedChange={(checked) => setState((current) => ({ ...current, isActive: checked }))} />
|
||||
<Switch
|
||||
checked={state.isActive}
|
||||
onCheckedChange={(checked) =>
|
||||
setState((current) => ({ ...current, isActive: checked }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button type='button' variant='outline' onClick={() => onOpenChange(false)}>
|
||||
@@ -147,7 +185,10 @@ function SequenceDialog({
|
||||
|
||||
export function DocumentSequenceSettings() {
|
||||
const { data } = useSuspenseQuery(documentSequencesQueryOptions());
|
||||
const [dialogState, setDialogState] = React.useState<{ open: boolean; sequence?: DocumentSequenceListItem }>({
|
||||
const [dialogState, setDialogState] = React.useState<{
|
||||
open: boolean;
|
||||
sequence?: DocumentSequenceListItem;
|
||||
}>({
|
||||
open: false
|
||||
});
|
||||
const resetMutation = useMutation({
|
||||
@@ -166,7 +207,10 @@ export function DocumentSequenceSettings() {
|
||||
<div className='flex items-center justify-between gap-4 rounded-lg border p-4'>
|
||||
<div>
|
||||
<div className='font-medium'>Document Sequence Admin</div>
|
||||
<div className='text-muted-foreground text-sm'>Maintain organization-scoped numbering strategy without rewriting historic document codes.</div>
|
||||
<div className='text-muted-foreground text-sm'>
|
||||
Maintain organization-scoped numbering strategy without rewriting historic document
|
||||
codes.
|
||||
</div>
|
||||
</div>
|
||||
<Button onClick={() => setDialogState({ open: true })}>
|
||||
<Icons.add className='mr-2 h-4 w-4' />
|
||||
@@ -185,7 +229,8 @@ export function DocumentSequenceSettings() {
|
||||
<div>
|
||||
<div className='text-xl font-semibold'>{sequence.documentType}</div>
|
||||
<div className='text-muted-foreground text-sm'>
|
||||
{sequence.prefix}{sequence.period} • branch {sequence.branchId || 'default'}
|
||||
{sequence.prefix}
|
||||
{sequence.period} • branch {sequence.branchId || 'default'}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex gap-2'>
|
||||
@@ -206,7 +251,9 @@ export function DocumentSequenceSettings() {
|
||||
</div>
|
||||
<div className='rounded-lg bg-muted/40 p-4'>
|
||||
<div className='text-muted-foreground text-xs'>Updated At</div>
|
||||
<div className='mt-1 text-sm font-medium'>{new Date(sequence.updatedAt).toLocaleString()}</div>
|
||||
<div className='mt-1 text-sm font-medium'>
|
||||
{formatDateTime(sequence.updatedAt)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='rounded-lg bg-muted/40 p-4'>
|
||||
<div className='text-muted-foreground text-xs'>Preview</div>
|
||||
@@ -229,7 +276,11 @@ export function DocumentSequenceSettings() {
|
||||
>
|
||||
Reset Counter
|
||||
</Button>
|
||||
<Button variant='outline' onClick={() => deleteMutation.mutate(sequence.id)} isLoading={deleteMutation.isPending}>
|
||||
<Button
|
||||
variant='outline'
|
||||
onClick={() => deleteMutation.mutate(sequence.id)}
|
||||
isLoading={deleteMutation.isPending}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user