251211:1622 Frontend: refactor Dashboard (not finish)
Some checks failed
Spec Validation / validate-markdown (push) Has been cancelled
Spec Validation / validate-diagrams (push) Has been cancelled
Spec Validation / check-todos (push) Has been cancelled

This commit is contained in:
admin
2025-12-11 16:22:50 +07:00
parent 3fa28bd14f
commit 2473c4c474
32 changed files with 1115 additions and 260 deletions

View File

@@ -25,6 +25,17 @@ import {
import { Plus, Pencil, Trash2, RefreshCw } from "lucide-react";
import { toast } from "sonner";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { Skeleton } from "@/components/ui/skeleton";
interface FieldConfig {
name: string;
@@ -66,6 +77,10 @@ export function GenericCrudTable({
const [editingItem, setEditingItem] = useState<any>(null);
const [formData, setFormData] = useState<any>({});
// Delete Dialog State
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [itemToDelete, setItemToDelete] = useState<number | null>(null);
const { data, isLoading, refetch } = useQuery({
queryKey,
queryFn: fetchFn,
@@ -96,6 +111,8 @@ export function GenericCrudTable({
onSuccess: () => {
toast.success(`${entityName} deleted successfully`);
queryClient.invalidateQueries({ queryKey });
setDeleteDialogOpen(false);
setItemToDelete(null);
},
onError: () => toast.error(`Failed to delete ${entityName}`),
});
@@ -112,9 +129,14 @@ export function GenericCrudTable({
setIsOpen(true);
};
const handleDelete = (id: number) => {
if (confirm(`Are you sure you want to delete this ${entityName}?`)) {
deleteMutation.mutate(id);
const handleDeleteClick = (id: number) => {
setItemToDelete(id);
setDeleteDialogOpen(true);
};
const confirmDelete = () => {
if (itemToDelete) {
deleteMutation.mutate(itemToDelete);
}
};
@@ -156,7 +178,7 @@ export function GenericCrudTable({
variant="ghost"
size="icon"
className="text-destructive"
onClick={() => handleDelete(row.original.id)}
onClick={() => handleDeleteClick(row.original.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
@@ -191,7 +213,17 @@ export function GenericCrudTable({
</div>
</div>
<DataTable columns={tableColumns} data={data || []} />
{isLoading ? (
<div className="space-y-2">
{[1, 2, 3, 4, 5].map((i) => (
<div key={i} className="flex items-center space-x-4">
<Skeleton className="h-12 w-full" />
</div>
))}
</div>
) : (
<DataTable columns={tableColumns} data={data || []} />
)}
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent>
@@ -270,6 +302,26 @@ export function GenericCrudTable({
</form>
</DialogContent>
</Dialog>
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete the {entityName.toLowerCase()} and remove it from the system.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={confirmDelete}
className="bg-red-600 hover:bg-red-700"
>
{deleteMutation.isPending ? "Deleting..." : "Delete"}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}

View File

@@ -5,6 +5,8 @@ import { Pagination } from "@/components/common/pagination";
import { useCorrespondences } from "@/hooks/use-correspondence";
import { useSearchParams } from "next/navigation";
import { Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import Link from "next/link";
export function CorrespondencesContent() {
const searchParams = useSearchParams();
@@ -12,10 +14,13 @@ export function CorrespondencesContent() {
const status = searchParams.get("status") || undefined;
const search = searchParams.get("search") || undefined;
const revisionStatus = (searchParams.get('revisionStatus') as 'CURRENT' | 'ALL' | 'OLD') || 'CURRENT';
const { data, isLoading, isError } = useCorrespondences({
page,
status,
search,
revisionStatus,
} as any);
if (isLoading) {
@@ -36,12 +41,27 @@ export function CorrespondencesContent() {
return (
<>
<CorrespondenceList data={data} />
<div className="mb-4 flex gap-2">
<div className="flex gap-1 bg-muted p-1 rounded-md">
{['ALL', 'CURRENT', 'OLD'].map((status) => (
<Link key={status} href={`?${new URLSearchParams({...Object.fromEntries(searchParams.entries()), revisionStatus: status, page: '1'}).toString()}`}>
<Button
variant={revisionStatus === status ? 'default' : 'ghost'}
size="sm"
className="text-xs px-3"
>
{status === 'CURRENT' ? 'Latest' : status === 'OLD' ? 'Previous' : 'All'}
</Button>
</Link>
))}
</div>
</div>
<CorrespondenceList data={data?.data || []} />
<div className="mt-4">
<Pagination
currentPage={data?.page || 1}
totalPages={data?.totalPages || 1}
total={data?.total || 0}
currentPage={data?.meta?.page || 1}
totalPages={data?.meta?.totalPages || 1}
total={data?.meta?.total || 0}
/>
</div>
</>

View File

@@ -1,11 +1,11 @@
"use client";
import { Correspondence, Attachment } from "@/types/correspondence";
import { Correspondence } from "@/types/correspondence";
import { StatusBadge } from "@/components/common/status-badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { format } from "date-fns";
import { ArrowLeft, Download, FileText, Loader2, Send, CheckCircle, XCircle } from "lucide-react";
import { ArrowLeft, Download, FileText, Loader2, Send, CheckCircle, XCircle, Edit } from "lucide-react";
import Link from "next/link";
import { useSubmitCorrespondence, useProcessWorkflow } from "@/hooks/use-correspondence";
import { useState } from "react";
@@ -22,11 +22,24 @@ export function CorrespondenceDetail({ data }: CorrespondenceDetailProps) {
const [actionState, setActionState] = useState<"approve" | "reject" | null>(null);
const [comments, setComments] = useState("");
if (!data) return <div>No data found</div>;
console.log("Correspondence Detail Data:", data);
// Derive Current Revision Data
const currentRevision = data.revisions?.find(r => r.isCurrent) || data.revisions?.[0];
const subject = currentRevision?.title || "-";
const description = currentRevision?.description || "-";
const status = currentRevision?.status?.statusCode || "UNKNOWN"; // e.g. DRAFT
const attachments = currentRevision?.attachments || [];
// Note: Importance might be in details
const importance = currentRevision?.details?.importance || "NORMAL";
const handleSubmit = () => {
if (confirm("Are you sure you want to submit this correspondence?")) {
// TODO: Implement Template Selection. Hardcoded to 1 for now.
submitMutation.mutate({
id: data.correspondenceId,
id: data.id,
data: {}
});
}
@@ -37,7 +50,7 @@ export function CorrespondenceDetail({ data }: CorrespondenceDetailProps) {
const action = actionState === "approve" ? "APPROVE" : "REJECT";
processMutation.mutate({
id: data.correspondenceId,
id: data.id,
data: {
action,
comments
@@ -61,20 +74,30 @@ export function CorrespondenceDetail({ data }: CorrespondenceDetailProps) {
</Button>
</Link>
<div>
<h1 className="text-2xl font-bold">{data.documentNumber}</h1>
<h1 className="text-2xl font-bold">{data.correspondenceNumber}</h1>
<p className="text-muted-foreground">
Created on {format(new Date(data.createdAt), "dd MMM yyyy HH:mm")}
Created on {data.createdAt ? format(new Date(data.createdAt), "dd MMM yyyy HH:mm") : '-'}
</p>
</div>
</div>
<div className="flex gap-2">
{data.status === "DRAFT" && (
{/* EDIT BUTTON LOGIC: Show if DRAFT */}
{status === "DRAFT" && (
<Link href={`/correspondences/${data.id}/edit`}>
<Button variant="outline">
<Edit className="mr-2 h-4 w-4" />
Edit
</Button>
</Link>
)}
{status === "DRAFT" && (
<Button onClick={handleSubmit} disabled={submitMutation.isPending}>
{submitMutation.isPending ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Send className="mr-2 h-4 w-4" />}
Submit for Review
</Button>
)}
{data.status === "IN_REVIEW" && (
{status === "IN_REVIEW" && (
<>
<Button
variant="destructive"
@@ -134,15 +157,15 @@ export function CorrespondenceDetail({ data }: CorrespondenceDetailProps) {
<Card>
<CardHeader>
<div className="flex justify-between items-start">
<CardTitle className="text-xl">{data.subject}</CardTitle>
<StatusBadge status={data.status} />
<CardTitle className="text-xl">{subject}</CardTitle>
<StatusBadge status={status} />
</div>
</CardHeader>
<CardContent className="space-y-6">
<div>
<h3 className="font-semibold mb-2">Description</h3>
<p className="text-gray-700 whitespace-pre-wrap">
{data.description || "No description provided."}
{description}
</p>
</div>
@@ -150,9 +173,9 @@ export function CorrespondenceDetail({ data }: CorrespondenceDetailProps) {
<div>
<h3 className="font-semibold mb-3">Attachments</h3>
{data.attachments && data.attachments.length > 0 ? (
{attachments && attachments.length > 0 ? (
<div className="grid gap-2">
{data.attachments.map((file, index) => (
{attachments.map((file, index) => (
<div
key={file.id || index}
className="flex items-center justify-between p-3 border rounded-lg bg-muted/20"
@@ -170,7 +193,7 @@ export function CorrespondenceDetail({ data }: CorrespondenceDetailProps) {
))}
</div>
) : (
<p className="text-sm text-muted-foreground italic">No attachments.</p>
<p className="text-sm text-muted-foreground italic">No attachments found.</p>
)}
</div>
</CardContent>
@@ -188,10 +211,10 @@ export function CorrespondenceDetail({ data }: CorrespondenceDetailProps) {
<p className="text-sm font-medium text-muted-foreground">Importance</p>
<div className="mt-1">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
${data.importance === 'URGENT' ? 'bg-red-100 text-red-800' :
data.importance === 'HIGH' ? 'bg-orange-100 text-orange-800' :
${importance === 'URGENT' ? 'bg-red-100 text-red-800' :
importance === 'HIGH' ? 'bg-orange-100 text-orange-800' :
'bg-blue-100 text-blue-800'}`}>
{data.importance}
{importance}
</span>
</div>
</div>
@@ -199,15 +222,15 @@ export function CorrespondenceDetail({ data }: CorrespondenceDetailProps) {
<hr className="my-4 border-t" />
<div>
<p className="text-sm font-medium text-muted-foreground">From Organization</p>
<p className="font-medium mt-1">{data.fromOrganization?.orgName}</p>
<p className="text-xs text-muted-foreground">{data.fromOrganization?.orgCode}</p>
<p className="text-sm font-medium text-muted-foreground">Originator</p>
<p className="font-medium mt-1">{data.originator?.orgName || '-'}</p>
<p className="text-xs text-muted-foreground">{data.originator?.orgCode || '-'}</p>
</div>
<div>
<p className="text-sm font-medium text-muted-foreground">To Organization</p>
<p className="font-medium mt-1">{data.toOrganization?.orgName}</p>
<p className="text-xs text-muted-foreground">{data.toOrganization?.orgCode}</p>
<div>
<p className="text-sm font-medium text-muted-foreground">Project</p>
<p className="font-medium mt-1">{data.project?.projectName || '-'}</p>
<p className="text-xs text-muted-foreground">{data.project?.projectCode || '-'}</p>
</div>
</CardContent>
</Card>

View File

@@ -17,7 +17,7 @@ import {
import { FileUpload } from "@/components/common/file-upload";
import { useRouter } from "next/navigation";
import { Loader2 } from "lucide-react";
import { useCreateCorrespondence } from "@/hooks/use-correspondence";
import { useCreateCorrespondence, useUpdateCorrespondence } from "@/hooks/use-correspondence";
import { Organization } from "@/types/organization";
import { useOrganizations } from "@/hooks/use-master-data";
import { CreateCorrespondenceDto } from "@/types/dto/correspondence/create-correspondence.dto";
@@ -34,50 +34,66 @@ const correspondenceSchema = z.object({
type FormData = z.infer<typeof correspondenceSchema>;
export function CorrespondenceForm() {
export function CorrespondenceForm({ initialData, id }: { initialData?: any, id?: number }) {
const router = useRouter();
const createMutation = useCreateCorrespondence();
const updateMutation = useUpdateCorrespondence(); // Add this hook
const { data: organizations, isLoading: isLoadingOrgs } = useOrganizations();
// Extract initial values if editing
const currentRev = initialData?.revisions?.find((r: any) => r.isCurrent) || initialData?.revisions?.[0];
const defaultValues: Partial<FormData> = {
subject: currentRev?.title || "",
description: currentRev?.description || "",
documentTypeId: initialData?.correspondenceTypeId || 1,
fromOrganizationId: initialData?.originatorId || undefined,
toOrganizationId: currentRev?.details?.to_organization_id || undefined,
importance: currentRev?.details?.importance || "NORMAL",
};
const {
register,
handleSubmit,
setValue,
watch, // Watch values to control Select value props if needed, or rely on RHF
formState: { errors },
} = useForm<FormData>({
resolver: zodResolver(correspondenceSchema),
defaultValues: {
importance: "NORMAL",
documentTypeId: 1,
} as any, // Cast to any to handle partial defaults for required fields
defaultValues: defaultValues as any,
});
// Watch for controlled inputs to set value in Select components if needed for better UX
const fromOrgId = watch("fromOrganizationId");
const toOrgId = watch("toOrganizationId");
const onSubmit = (data: FormData) => {
// Map FormData to CreateCorrespondenceDto
// Note: projectId is hardcoded to 1 for now as per requirements/context
const payload: CreateCorrespondenceDto = {
projectId: 1,
typeId: data.documentTypeId,
title: data.subject,
description: data.description,
originatorId: data.fromOrganizationId, // Mapping From -> Originator (Impersonation)
originatorId: data.fromOrganizationId,
details: {
to_organization_id: data.toOrganizationId,
importance: data.importance
},
// create-correspondence DTO does not have 'attachments' field at root usually, often handled separate or via multipart
// If useCreateCorrespondence handles multipart, we might need to pass FormData object or specific structure
// For now, aligning with DTO interface.
};
// If the hook expects the DTO directly:
createMutation.mutate(payload, {
onSuccess: () => {
router.push("/correspondences");
},
});
if (id && initialData) {
// UPDATE Mode
updateMutation.mutate({ id, data: payload }, {
onSuccess: () => router.push(`/correspondences/${id}`)
});
} else {
// CREATE Mode
createMutation.mutate(payload, {
onSuccess: () => router.push("/correspondences"),
});
}
};
const isPending = createMutation.isPending || updateMutation.isPending;
return (
<form onSubmit={handleSubmit(onSubmit)} className="max-w-3xl space-y-6">
<div className="space-y-2">
@@ -103,6 +119,7 @@ export function CorrespondenceForm() {
<Label>From Organization *</Label>
<Select
onValueChange={(v) => setValue("fromOrganizationId", parseInt(v))}
value={fromOrgId ? String(fromOrgId) : undefined}
disabled={isLoadingOrgs}
>
<SelectTrigger>
@@ -125,6 +142,7 @@ export function CorrespondenceForm() {
<Label>To Organization *</Label>
<Select
onValueChange={(v) => setValue("toOrganizationId", parseInt(v))}
value={toOrgId ? String(toOrgId) : undefined}
disabled={isLoadingOrgs}
>
<SelectTrigger>
@@ -177,22 +195,24 @@ export function CorrespondenceForm() {
</div>
</div>
<div className="space-y-2">
<Label>Attachments</Label>
<FileUpload
onFilesSelected={(files) => setValue("attachments", files)}
maxFiles={10}
accept=".pdf,.doc,.docx,.xls,.xlsx,.jpg,.png"
/>
</div>
{!initialData && (
<div className="space-y-2">
<Label>Attachments</Label>
<FileUpload
onFilesSelected={(files) => setValue("attachments", files)}
maxFiles={10}
accept=".pdf,.doc,.docx,.xls,.xlsx,.jpg,.png"
/>
</div>
)}
<div className="flex justify-end gap-4 pt-6 border-t">
<Button type="button" variant="outline" onClick={() => router.back()}>
Cancel
</Button>
<Button type="submit" disabled={createMutation.isPending}>
{createMutation.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Create Correspondence
<Button type="submit" disabled={isPending}>
{isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{id ? "Update Correspondence" : "Create Correspondence"}
</Button>
</div>
</form>

View File

@@ -1,48 +1,49 @@
"use client";
import { Correspondence } from "@/types/correspondence";
import { CorrespondenceRevision } from "@/types/correspondence";
import { DataTable } from "@/components/common/data-table";
import { ColumnDef } from "@tanstack/react-table";
import { StatusBadge } from "@/components/common/status-badge";
import { Button } from "@/components/ui/button";
import { Eye, Edit } from "lucide-react";
import { Eye, Edit, FileText } from "lucide-react";
import Link from "next/link";
import { format } from "date-fns";
interface CorrespondenceListProps {
data?: {
items: Correspondence[];
total: number;
page: number;
totalPages: number;
};
data: CorrespondenceRevision[];
}
export function CorrespondenceList({ data }: CorrespondenceListProps) {
const columns: ColumnDef<Correspondence>[] = [
const columns: ColumnDef<CorrespondenceRevision>[] = [
{
accessorKey: "documentNumber",
accessorKey: "correspondence.correspondenceNumber",
header: "Document No.",
cell: ({ row }) => (
<span className="font-medium">{row.getValue("documentNumber")}</span>
<span className="font-medium">{row.original.correspondence?.correspondenceNumber}</span>
),
},
{
accessorKey: "subject",
accessorKey: "revisionLabel",
header: "Rev",
cell: ({ row }) => (
<span className="font-medium">{row.original.revisionLabel || row.original.revisionNumber}</span>
),
},
{
accessorKey: "title",
header: "Subject",
cell: ({ row }) => (
<div className="max-w-[300px] truncate" title={row.getValue("subject")}>
{row.getValue("subject")}
<div className="max-w-[300px] truncate" title={row.original.title}>
{row.original.title}
</div>
),
},
{
accessorKey: "fromOrganization.orgName",
accessorKey: "correspondence.originator.orgName",
header: "From",
},
{
accessorKey: "toOrganization.orgName",
header: "To",
cell: ({ row }) => (
<span>{row.original.correspondence?.originator?.orgName || '-'}</span>
),
},
{
accessorKey: "createdAt",
@@ -50,23 +51,45 @@ export function CorrespondenceList({ data }: CorrespondenceListProps) {
cell: ({ row }) => format(new Date(row.getValue("createdAt")), "dd MMM yyyy"),
},
{
accessorKey: "status",
accessorKey: "status.statusName",
header: "Status",
cell: ({ row }) => <StatusBadge status={row.getValue("status")} />,
cell: ({ row }) => <StatusBadge status={row.original.status?.statusCode || "UNKNOWN"} />,
},
{
id: "actions",
cell: ({ row }) => {
const item = row.original;
// Edit/View link goes to the DOCUMENT detail (correspondence.id)
// Ideally we might pass ?revId=item.id to view specific revision, but detail page defaults to latest.
// For editing, we edit the document.
const docId = item.correspondence.id;
const statusCode = item.status?.statusCode;
return (
<div className="flex gap-2">
<Link href={`/correspondences/${row.original.correspondenceId}`}>
<Button variant="ghost" size="icon" title="View">
<Link href={`/correspondences/${docId}`}>
<Button variant="ghost" size="icon" title="View Details">
<Eye className="h-4 w-4" />
</Button>
</Link>
{item.status === "DRAFT" && (
<Link href={`/correspondences/${row.original.correspondenceId}/edit`}>
<Button variant="ghost" size="icon" title="View File" onClick={() => {
const attachments = item.attachments; // Now we are on Revision, so attachments might be here if joined
if (attachments && attachments.length > 0 && attachments[0].url) {
window.open(attachments[0].url, '_blank');
} else {
// Fallback check if attachments are on details json inside revision
// or if we simply didn't join them yet.
// Current Backend join: leftJoinAndSelect('rev.status', 'status') doesn't join attachments explicitly but maybe relation exists?
// Wait, checking Entity... CorrespondenceRevision does NOT have attachments relation in code snippet provided earlier.
// It might be in 'details' JSON or implied.
// Just Alert for now as per previous logic.
alert("ไม่พบไฟล์แนบ (No file attached)");
}
}}>
<FileText className="h-4 w-4" />
</Button>
{statusCode === "DRAFT" && (
<Link href={`/correspondences/${docId}/edit`}>
<Button variant="ghost" size="icon" title="Edit">
<Edit className="h-4 w-4" />
</Button>
@@ -80,8 +103,7 @@ export function CorrespondenceList({ data }: CorrespondenceListProps) {
return (
<div>
<DataTable columns={columns} data={data?.items || []} />
{/* Pagination component would go here, receiving props from data */}
<DataTable columns={columns} data={data || []} />
</div>
);
}

View File

@@ -22,14 +22,14 @@ export function StatsCards({ stats, isLoading }: StatsCardsProps) {
const cards = [
{
title: "Total Correspondences",
value: stats.correspondences,
value: stats.totalDocuments,
icon: FileText,
color: "text-blue-600",
bgColor: "bg-blue-50",
},
{
title: "Active RFAs",
value: stats.rfas,
value: stats.totalRfas,
icon: Clipboard,
color: "text-purple-600",
bgColor: "bg-purple-50",
@@ -43,7 +43,7 @@ export function StatsCards({ stats, isLoading }: StatsCardsProps) {
},
{
title: "Pending Approvals",
value: stats.pending,
value: stats.pendingApprovals,
icon: Clock,
color: "text-orange-600",
bgColor: "bg-orange-50",

View File

@@ -5,17 +5,12 @@ import { DataTable } from "@/components/common/data-table";
import { ColumnDef } from "@tanstack/react-table";
import { StatusBadge } from "@/components/common/status-badge";
import { Button } from "@/components/ui/button";
import { Eye } from "lucide-react";
import { Eye, Edit, FileText } from "lucide-react";
import Link from "next/link";
import { format } from "date-fns";
interface RFAListProps {
data: {
items: RFA[];
total: number;
page: number;
totalPages: number;
};
data: RFA[];
}
export function RFAList({ data }: RFAListProps) {
@@ -25,48 +20,87 @@ export function RFAList({ data }: RFAListProps) {
{
accessorKey: "rfa_number",
header: "RFA No.",
cell: ({ row }) => (
<span className="font-medium">{row.getValue("rfa_number")}</span>
),
cell: ({ row }) => {
const rev = row.original.revisions?.[0];
return <span className="font-medium">{rev?.correspondence?.correspondenceNumber || '-'}</span>;
},
},
{
accessorKey: "subject",
header: "Subject",
cell: ({ row }) => (
<div className="max-w-[300px] truncate" title={row.getValue("subject")}>
{row.getValue("subject")}
</div>
),
cell: ({ row }) => {
const rev = row.original.revisions?.[0];
return (
<div className="max-w-[300px] truncate" title={rev?.title}>
{rev?.title || '-'}
</div>
);
},
},
{
accessorKey: "contract_name",
accessorKey: "contract_name", // AccessorKey can be anything if we provide cell
header: "Contract",
cell: ({ row }) => {
const rev = row.original.revisions?.[0];
return <span>{rev?.correspondence?.project?.projectName || '-'}</span>;
},
},
{
accessorKey: "discipline_name",
header: "Discipline",
cell: ({ row }) => <span>{row.original.discipline?.name || '-'}</span>,
},
{
accessorKey: "createdAt",
header: "Created",
cell: ({ row }) => format(new Date(row.getValue("createdAt")), "dd MMM yyyy"),
cell: ({ row }) => {
const date = row.original.revisions?.[0]?.correspondence?.createdAt;
return date ? format(new Date(date), "dd MMM yyyy") : '-';
},
},
{
accessorKey: "status",
header: "Status",
cell: ({ row }) => <StatusBadge status={row.getValue("status")} />,
cell: ({ row }) => {
const status = row.original.revisions?.[0]?.statusCode?.statusName || row.original.revisions?.[0]?.statusCode?.statusCode;
return <StatusBadge status={status || 'Unknown'} />;
},
},
{
id: "actions",
cell: ({ row }) => {
const item = row.original;
const handleViewFile = (e: React.MouseEvent) => {
e.preventDefault();
// Logic to find first attachment: Check items -> shopDrawingRevision -> attachments
const firstAttachment = item.revisions?.[0]?.items?.[0]?.shopDrawingRevision?.attachments?.[0];
if (firstAttachment?.url) {
window.open(firstAttachment.url, '_blank');
} else {
// Use alert or toast. Assuming toast is available or use generic alert for now if toast not imported
// But rfa.service.ts in use-rfa.ts uses 'sonner', so 'sonner' is likely available.
// I will try to use toast from 'sonner' if I import it, or just window.alert for safety.
// User said "หน้าต่างแจ้งเตือน" -> Alert window.
alert("ไม่พบไฟล์แนบ (No file attached)");
}
};
return (
<div className="flex gap-2">
<Link href={`/rfas/${row.original.rfaId}`}>
<Button variant="ghost" size="icon" title="View">
<Link href={`/rfas/${row.original.id}`}>
<Button variant="ghost" size="icon" title="View Details">
<Eye className="h-4 w-4" />
</Button>
</Link>
<Button variant="ghost" size="icon" title="View File" onClick={handleViewFile}>
<FileText className="h-4 w-4" />
</Button>
<Link href={`/rfas/${row.original.id}/edit`}>
<Button variant="ghost" size="icon" title="Edit">
<Edit className="h-4 w-4" />
</Button>
</Link>
</div>
);
},
@@ -75,7 +109,7 @@ export function RFAList({ data }: RFAListProps) {
return (
<div>
<DataTable columns={columns} data={data?.items || []} />
<DataTable columns={columns} data={data || []} />
{/* Pagination component would go here */}
</div>
);

View File

@@ -0,0 +1,15 @@
import { cn } from "@/lib/utils"
function Skeleton({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-muted", className)}
{...props}
/>
)
}
export { Skeleton }