task-fixdatetime

This commit is contained in:
phaichayon
2026-06-25 21:09:27 +07:00
parent 3bf3405c48
commit 14acee1127
36 changed files with 4788 additions and 1011 deletions

View File

@@ -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)
}
];
}

View File

@@ -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>

View File

@@ -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>