251208:1625 Frontend: to be complete admin panel, Backend: tobe recheck all task
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user