251208:1625 Frontend: to be complete admin panel, Backend: tobe recheck all task
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-08 16:25:56 +07:00
parent dcd126d704
commit 863a727756
64 changed files with 5956 additions and 1256 deletions

View File

@@ -1,19 +1,55 @@
"use client";
import { Correspondence } from "@/types/correspondence";
import { Correspondence, Attachment } 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 } from "lucide-react";
import { ArrowLeft, Download, FileText, Loader2, Send, CheckCircle, XCircle } from "lucide-react";
import Link from "next/link";
import { Separator } from "@/components/ui/separator";
import { useSubmitCorrespondence, useProcessWorkflow } from "@/hooks/use-correspondence";
import { useState } from "react";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
interface CorrespondenceDetailProps {
data: Correspondence;
}
export function CorrespondenceDetail({ data }: CorrespondenceDetailProps) {
const submitMutation = useSubmitCorrespondence();
const processMutation = useProcessWorkflow();
const [actionState, setActionState] = useState<"approve" | "reject" | null>(null);
const [comments, setComments] = useState("");
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.correspondence_id,
data: { templateId: 1 }
});
}
};
const handleProcess = () => {
if (!actionState) return;
const action = actionState === "approve" ? "APPROVE" : "REJECT";
processMutation.mutate({
id: data.correspondence_id,
data: {
action,
comments
}
}, {
onSuccess: () => {
setActionState(null);
setComments("");
}
});
};
return (
<div className="space-y-6">
{/* Header / Actions */}
@@ -32,19 +68,66 @@ export function CorrespondenceDetail({ data }: CorrespondenceDetailProps) {
</div>
</div>
<div className="flex gap-2">
{/* Workflow Actions Placeholder */}
{data.status === "DRAFT" && (
<Button>Submit for Review</Button>
<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" && (
<>
<Button variant="destructive">Reject</Button>
<Button className="bg-green-600 hover:bg-green-700">Approve</Button>
<Button
variant="destructive"
onClick={() => setActionState("reject")}
>
<XCircle className="mr-2 h-4 w-4" />
Reject
</Button>
<Button
className="bg-green-600 hover:bg-green-700"
onClick={() => setActionState("approve")}
>
<CheckCircle className="mr-2 h-4 w-4" />
Approve
</Button>
</>
)}
</div>
</div>
{/* Action Input Area */}
{actionState && (
<Card className="border-primary">
<CardHeader>
<CardTitle className="text-lg">
{actionState === "approve" ? "Confirm Approval" : "Confirm Rejection"}
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label>Comments</Label>
<Textarea
value={comments}
onChange={(e) => setComments(e.target.value)}
placeholder="Enter comments..."
/>
</div>
<div className="flex justify-end gap-2">
<Button variant="ghost" onClick={() => setActionState(null)}>Cancel</Button>
<Button
variant={actionState === "approve" ? "default" : "destructive"}
onClick={handleProcess}
disabled={processMutation.isPending}
className={actionState === "approve" ? "bg-green-600 hover:bg-green-700" : ""}
>
{processMutation.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Confirm {actionState === "approve" ? "Approve" : "Reject"}
</Button>
</div>
</CardContent>
</Card>
)}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Main Content */}
<div className="lg:col-span-2 space-y-6">
@@ -63,23 +146,25 @@ export function CorrespondenceDetail({ data }: CorrespondenceDetailProps) {
</p>
</div>
<Separator />
<hr className="my-4 border-t" />
<div>
<h3 className="font-semibold mb-3">Attachments</h3>
{data.attachments && data.attachments.length > 0 ? (
<div className="grid gap-2">
{data.attachments.map((file: any, index: number) => (
{data.attachments.map((file, index) => (
<div
key={index}
key={file.id || index}
className="flex items-center justify-between p-3 border rounded-lg bg-muted/20"
>
<div className="flex items-center gap-3">
<FileText className="h-5 w-5 text-primary" />
<span className="text-sm font-medium">{file.name || `Attachment ${index + 1}`}</span>
<span className="text-sm font-medium">{file.name}</span>
</div>
<Button variant="ghost" size="sm">
<Download className="h-4 w-4" />
<Button variant="ghost" size="sm" asChild>
<a href={file.url} target="_blank" rel="noopener noreferrer">
<Download className="h-4 w-4" />
</a>
</Button>
</div>
))}
@@ -111,7 +196,7 @@ export function CorrespondenceDetail({ data }: CorrespondenceDetailProps) {
</div>
</div>
<Separator />
<hr className="my-4 border-t" />
<div>
<p className="text-sm font-medium text-muted-foreground">From Organization</p>

View File

@@ -19,11 +19,12 @@ import { useRouter } from "next/navigation";
import { Loader2 } from "lucide-react";
import { useCreateCorrespondence } from "@/hooks/use-correspondence";
import { useOrganizations } from "@/hooks/use-master-data";
import { CreateCorrespondenceDto } from "@/types/dto/correspondence/create-correspondence.dto";
const correspondenceSchema = z.object({
subject: z.string().min(5, "Subject must be at least 5 characters"),
description: z.string().optional(),
document_type_id: z.number().default(1), // Default to General for now
document_type_id: z.number().default(1),
from_organization_id: z.number({ required_error: "Please select From Organization" }),
to_organization_id: z.number({ required_error: "Please select To Organization" }),
importance: z.enum(["NORMAL", "HIGH", "URGENT"]).default("NORMAL"),
@@ -41,18 +42,38 @@ export function CorrespondenceForm() {
register,
handleSubmit,
setValue,
watch,
formState: { errors },
} = useForm<FormData>({
resolver: zodResolver(correspondenceSchema),
defaultValues: {
importance: "NORMAL",
document_type_id: 1,
// @ts-ignore: Intentionally undefined for required fields to force selection
from_organization_id: undefined,
to_organization_id: undefined,
},
});
const onSubmit = (data: FormData) => {
createMutation.mutate(data as any, {
// Map FormData to CreateCorrespondenceDto
// Note: projectId is hardcoded to 1 for now as per requirements/context
const payload: CreateCorrespondenceDto = {
projectId: 1,
typeId: data.document_type_id,
title: data.subject,
description: data.description,
originatorId: data.from_organization_id, // Mapping From -> Originator (Impersonation)
details: {
to_organization_id: data.to_organization_id,
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");
},
@@ -61,7 +82,6 @@ export function CorrespondenceForm() {
return (
<form onSubmit={handleSubmit(onSubmit)} className="max-w-3xl space-y-6">
{/* Subject */}
<div className="space-y-2">
<Label htmlFor="subject">Subject *</Label>
<Input id="subject" {...register("subject")} placeholder="Enter subject" />
@@ -70,7 +90,6 @@ export function CorrespondenceForm() {
)}
</div>
{/* Description */}
<div className="space-y-2">
<Label htmlFor="description">Description</Label>
<Textarea
@@ -81,7 +100,6 @@ export function CorrespondenceForm() {
/>
</div>
{/* From/To Organizations */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<Label>From Organization *</Label>
@@ -93,9 +111,9 @@ export function CorrespondenceForm() {
<SelectValue placeholder={isLoadingOrgs ? "Loading..." : "Select Organization"} />
</SelectTrigger>
<SelectContent>
{organizations?.map((org: any) => (
<SelectItem key={org.id} value={String(org.id)}>
{org.name || org.org_name} ({org.code || org.org_code})
{organizations?.map((org) => (
<SelectItem key={org.organization_id} value={String(org.organization_id)}>
{org.org_name} ({org.org_code})
</SelectItem>
))}
</SelectContent>
@@ -115,9 +133,9 @@ export function CorrespondenceForm() {
<SelectValue placeholder={isLoadingOrgs ? "Loading..." : "Select Organization"} />
</SelectTrigger>
<SelectContent>
{organizations?.map((org: any) => (
<SelectItem key={org.id} value={String(org.id)}>
{org.name || org.org_name} ({org.code || org.org_code})
{organizations?.map((org) => (
<SelectItem key={org.organization_id} value={String(org.organization_id)}>
{org.org_name} ({org.org_code})
</SelectItem>
))}
</SelectContent>
@@ -128,7 +146,6 @@ export function CorrespondenceForm() {
</div>
</div>
{/* Importance */}
<div className="space-y-2">
<Label>Importance</Label>
<div className="flex gap-6 mt-2">
@@ -162,7 +179,6 @@ export function CorrespondenceForm() {
</div>
</div>
{/* File Attachments */}
<div className="space-y-2">
<Label>Attachments</Label>
<FileUpload
@@ -172,7 +188,6 @@ export function CorrespondenceForm() {
/>
</div>
{/* Actions */}
<div className="flex justify-end gap-4 pt-6 border-t">
<Button type="button" variant="outline" onClick={() => router.back()}>
Cancel