summaryrefslogtreecommitdiff
path: root/components/form-data-plant/publish-dialog.tsx
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-10-23 10:10:21 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-10-23 10:10:21 +0000
commitf7f5069a2209cfa39b65f492f32270a5f554bed0 (patch)
tree933c731ec2cb7d8bc62219a0aeed45a5e97d5f15 /components/form-data-plant/publish-dialog.tsx
parentd49ad5dee1e5a504e1321f6db802b647497ee9ff (diff)
(대표님) EDP 해양 관련 개발 사항들
Diffstat (limited to 'components/form-data-plant/publish-dialog.tsx')
-rw-r--r--components/form-data-plant/publish-dialog.tsx470
1 files changed, 470 insertions, 0 deletions
diff --git a/components/form-data-plant/publish-dialog.tsx b/components/form-data-plant/publish-dialog.tsx
new file mode 100644
index 00000000..a3a2ef0b
--- /dev/null
+++ b/components/form-data-plant/publish-dialog.tsx
@@ -0,0 +1,470 @@
+"use client";
+
+import React, { useState, useEffect } from "react";
+import { useSession } from "next-auth/react";
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogDescription,
+ DialogFooter,
+} from "@/components/ui/dialog";
+import {
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+} from "@/components/ui/command";
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/ui/popover";
+import { Label } from "@/components/ui/label";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { Input } from "@/components/ui/input";
+import { Textarea } from "@/components/ui/textarea";
+import { Button } from "@/components/ui/button";
+import { Loader2, Check, ChevronsUpDown } from "lucide-react";
+import { toast } from "sonner";
+import { cn } from "@/lib/utils";
+import {
+ createRevisionAction,
+ fetchDocumentsByPackageId,
+ fetchStagesByDocumentId,
+ fetchRevisionsByStageParams,
+ Document,
+ IssueStage,
+ Revision
+} from "@/lib/vendor-document/service";
+
+interface PublishDialogProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ packageId: number;
+ formCode: string;
+ fileBlob?: Blob;
+}
+
+export const PublishDialog: React.FC<PublishDialogProps> = ({
+ open,
+ onOpenChange,
+ packageId,
+ formCode,
+ fileBlob,
+}) => {
+ // Get current user session from next-auth
+ const { data: session } = useSession();
+
+ // State for form data
+ const [documents, setDocuments] = useState<Document[]>([]);
+ const [stages, setStages] = useState<IssueStage[]>([]);
+ const [latestRevision, setLatestRevision] = useState<string>("");
+
+ // State for document search
+ const [openDocumentCombobox, setOpenDocumentCombobox] = useState(false);
+ const [documentSearchValue, setDocumentSearchValue] = useState("");
+
+ // Selected values
+ const [selectedDocId, setSelectedDocId] = useState<string>("");
+ const [selectedDocumentDisplay, setSelectedDocumentDisplay] = useState<string>("");
+ const [selectedStage, setSelectedStage] = useState<string>("");
+ const [revisionInput, setRevisionInput] = useState<string>("");
+ const [uploaderName, setUploaderName] = useState<string>("");
+ const [comment, setComment] = useState<string>("");
+ const [customFileName, setCustomFileName] = useState<string>(`${formCode}_document.docx`);
+
+ // Loading states
+ const [isLoading, setIsLoading] = useState<boolean>(false);
+ const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
+
+ // Filter documents by search
+ const filteredDocuments = documentSearchValue
+ ? documents.filter(doc =>
+ doc.docNumber.toLowerCase().includes(documentSearchValue.toLowerCase()) ||
+ doc.title.toLowerCase().includes(documentSearchValue.toLowerCase())
+ )
+ : documents;
+
+ // Set uploader name from session when dialog opens
+ useEffect(() => {
+ if (open && session?.user?.name) {
+ setUploaderName(session.user.name);
+ }
+ }, [open, session]);
+
+ // Reset all fields when dialog opens/closes
+ useEffect(() => {
+ if (open) {
+ setSelectedDocId("");
+ setSelectedDocumentDisplay("");
+ setSelectedStage("");
+ setRevisionInput("");
+ // Only set uploaderName if not already set from session
+ if (!session?.user?.name) setUploaderName("");
+ setComment("");
+ setLatestRevision("");
+ setCustomFileName(`${formCode}_document.docx`);
+ setDocumentSearchValue("");
+ }
+ }, [open, formCode, session]);
+
+ // Fetch documents based on packageId
+ useEffect(() => {
+ async function loadDocuments() {
+ if (packageId && open) {
+ setIsLoading(true);
+
+ try {
+ const docs = await fetchDocumentsByPackageId(packageId);
+ setDocuments(docs);
+ } catch (error) {
+ console.error("Error fetching documents:", error);
+ toast.error("Failed to load documents");
+ } finally {
+ setIsLoading(false);
+ }
+ }
+ }
+
+ loadDocuments();
+ }, [packageId, open]);
+
+ // Fetch stages when document is selected
+ useEffect(() => {
+ async function loadStages() {
+ if (selectedDocId) {
+ setIsLoading(true);
+
+ // Reset dependent fields
+ setSelectedStage("");
+ setRevisionInput("");
+ setLatestRevision("");
+
+ try {
+ const stagesList = await fetchStagesByDocumentId(parseInt(selectedDocId, 10));
+ setStages(stagesList);
+ } catch (error) {
+ console.error("Error fetching stages:", error);
+ toast.error("Failed to load stages");
+ } finally {
+ setIsLoading(false);
+ }
+ } else {
+ setStages([]);
+ }
+ }
+
+ loadStages();
+ }, [selectedDocId]);
+
+ // Fetch latest revision when stage is selected (for reference)
+ useEffect(() => {
+ async function loadLatestRevision() {
+ if (selectedDocId && selectedStage) {
+ setIsLoading(true);
+
+ try {
+ const revsList = await fetchRevisionsByStageParams(
+ parseInt(selectedDocId, 10),
+ selectedStage
+ );
+
+ // Find the latest revision (assuming revisions are sorted by revision number)
+ if (revsList.length > 0) {
+ // Sort revisions if needed
+ const sortedRevisions = [...revsList].sort((a, b) => {
+ return b.revision.localeCompare(a.revision, undefined, { numeric: true });
+ });
+
+ setLatestRevision(sortedRevisions[0].revision);
+
+ // Pre-fill the revision input with an incremented value if possible
+ if (sortedRevisions[0].revision.match(/^\d+$/)) {
+ // If it's a number, increment it
+ const nextRevision = String(parseInt(sortedRevisions[0].revision, 10) + 1);
+ setRevisionInput(nextRevision);
+ } else if (sortedRevisions[0].revision.match(/^[A-Za-z]$/)) {
+ // If it's a single letter, get the next letter
+ const currentChar = sortedRevisions[0].revision.charCodeAt(0);
+ const nextChar = String.fromCharCode(currentChar + 1);
+ setRevisionInput(nextChar);
+ } else {
+ // For other formats, just show the latest as reference
+ setRevisionInput("");
+ }
+ } else {
+ // If no revisions exist, set default values
+ setLatestRevision("");
+ setRevisionInput("0");
+ }
+ } catch (error) {
+ console.error("Error fetching revisions:", error);
+ toast.error("Failed to load revision information");
+ } finally {
+ setIsLoading(false);
+ }
+ } else {
+ setLatestRevision("");
+ setRevisionInput("");
+ }
+ }
+
+ loadLatestRevision();
+ }, [selectedDocId, selectedStage]);
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ if (!selectedDocId || !selectedStage || !revisionInput || !fileBlob) {
+ toast.error("Please fill in all required fields");
+ return;
+ }
+
+ setIsSubmitting(true);
+
+ try {
+ // Create FormData
+ const formData = new FormData();
+ formData.append("documentId", selectedDocId);
+ formData.append("stage", selectedStage);
+ formData.append("revision", revisionInput);
+ formData.append("customFileName", customFileName);
+ formData.append("uploaderType", "vendor"); // Default value
+
+ if (uploaderName) {
+ formData.append("uploaderName", uploaderName);
+ }
+
+ if (comment) {
+ formData.append("comment", comment);
+ }
+
+ // Append file as attachment
+ if (fileBlob) {
+ const file = new File([fileBlob], customFileName, {
+ type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
+ });
+ formData.append("attachment", file);
+ }
+
+ // Call server action directly
+ const result = await createRevisionAction(formData);
+
+ if (result) {
+ toast.success("Document published successfully!");
+ onOpenChange(false);
+ }
+ } catch (error) {
+ console.error("Error publishing document:", error);
+ toast.error("Failed to publish document");
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ return (
+ <Dialog open={open} onOpenChange={onOpenChange}>
+ <DialogContent className="sm:max-w-[500px]">
+ <DialogHeader>
+ <DialogTitle>Publish Document</DialogTitle>
+ <DialogDescription>
+ Select document, stage, and revision to publish the vendor document.
+ </DialogDescription>
+ </DialogHeader>
+
+ <form onSubmit={handleSubmit}>
+ <div className="grid gap-4 py-4">
+ {/* Document Selection with Search */}
+ <div className="grid grid-cols-4 items-center gap-4">
+ <Label htmlFor="document" className="text-right">
+ Document
+ </Label>
+ <div className="col-span-3">
+ <Popover
+ open={openDocumentCombobox}
+ onOpenChange={setOpenDocumentCombobox}
+ >
+ <PopoverTrigger asChild>
+ <Button
+ variant="outline"
+ role="combobox"
+ aria-expanded={openDocumentCombobox}
+ className="w-full justify-between"
+ disabled={isLoading || documents.length === 0}
+ >
+ {/* Add text-overflow handling for selected document display */}
+ <span className="truncate">
+ {selectedDocumentDisplay
+ ? selectedDocumentDisplay
+ : "Select document..."}
+ </span>
+ <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
+ </Button>
+ </PopoverTrigger>
+ <PopoverContent className="w-[400px] p-0">
+ <Command>
+ <CommandInput
+ placeholder="Search document..."
+ value={documentSearchValue}
+ onValueChange={setDocumentSearchValue}
+ />
+ <CommandEmpty>No document found.</CommandEmpty>
+ <CommandGroup className="max-h-[300px] overflow-auto">
+ {filteredDocuments.map((doc) => (
+ <CommandItem
+ key={doc.id}
+ value={`${doc.docNumber} - ${doc.title}`}
+ onSelect={() => {
+ setSelectedDocId(String(doc.id));
+ setSelectedDocumentDisplay(`${doc.docNumber} - ${doc.title}`);
+ setOpenDocumentCombobox(false);
+ }}
+ className="flex items-center"
+ >
+ <Check
+ className={cn(
+ "mr-2 h-4 w-4 flex-shrink-0",
+ selectedDocId === String(doc.id)
+ ? "opacity-100"
+ : "opacity-0"
+ )}
+ />
+ {/* Add text-overflow handling for document items */}
+ <span className="truncate">{doc.docNumber} - {doc.title}</span>
+ </CommandItem>
+ ))}
+ </CommandGroup>
+ </Command>
+ </PopoverContent>
+ </Popover>
+ </div>
+ </div>
+
+ {/* Stage Selection */}
+ <div className="grid grid-cols-4 items-center gap-4">
+ <Label htmlFor="stage" className="text-right">
+ Stage
+ </Label>
+ <div className="col-span-3">
+ <Select
+ value={selectedStage}
+ onValueChange={setSelectedStage}
+ disabled={isLoading || !selectedDocId || stages.length === 0}
+ >
+ <SelectTrigger>
+ <SelectValue placeholder="Select stage" />
+ </SelectTrigger>
+ <SelectContent>
+ {stages.map((stage) => (
+ <SelectItem key={stage.id} value={stage.stageName}>
+ {/* Add text-overflow handling for stage names */}
+ <span className="truncate">{stage.stageName}</span>
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ </div>
+ </div>
+
+ {/* Revision Input */}
+ <div className="grid grid-cols-4 items-center gap-4">
+ <Label htmlFor="revision" className="text-right">
+ Revision
+ </Label>
+ <div className="col-span-3">
+ <Input
+ id="revision"
+ value={revisionInput}
+ onChange={(e) => setRevisionInput(e.target.value)}
+ placeholder="Enter revision"
+ disabled={isLoading || !selectedStage}
+ />
+ {latestRevision && (
+ <p className="text-xs text-muted-foreground mt-1">
+ Latest revision: {latestRevision}
+ </p>
+ )}
+ </div>
+ </div>
+
+ <div className="grid grid-cols-4 items-center gap-4">
+ <Label htmlFor="fileName" className="text-right">
+ File Name
+ </Label>
+ <div className="col-span-3">
+ <Input
+ id="fileName"
+ value={customFileName}
+ onChange={(e) => setCustomFileName(e.target.value)}
+ placeholder="Custom file name"
+ />
+ </div>
+ </div>
+
+ <div className="grid grid-cols-4 items-center gap-4">
+ <Label htmlFor="uploaderName" className="text-right">
+ Uploader
+ </Label>
+ <div className="col-span-3">
+ <Input
+ id="uploaderName"
+ value={uploaderName}
+ onChange={(e) => setUploaderName(e.target.value)}
+ placeholder="Your name"
+ // Disable input but show a filled style
+ className={session?.user?.name ? "opacity-70" : ""}
+ readOnly={!!session?.user?.name}
+ />
+ {session?.user?.name && (
+ <p className="text-xs text-muted-foreground mt-1">
+ Using your account name from login
+ </p>
+ )}
+ </div>
+ </div>
+
+ <div className="grid grid-cols-4 items-center gap-4">
+ <Label htmlFor="comment" className="text-right">
+ Comment
+ </Label>
+ <div className="col-span-3">
+ <Textarea
+ id="comment"
+ value={comment}
+ onChange={(e) => setComment(e.target.value)}
+ placeholder="Optional comment"
+ className="resize-none"
+ />
+ </div>
+ </div>
+ </div>
+
+ <DialogFooter>
+ <Button
+ type="submit"
+ disabled={isSubmitting || !selectedDocId || !selectedStage || !revisionInput}
+ >
+ {isSubmitting ? (
+ <>
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+ Publishing...
+ </>
+ ) : (
+ "Publish"
+ )}
+ </Button>
+ </DialogFooter>
+ </form>
+ </DialogContent>
+ </Dialog>
+ );
+}; \ No newline at end of file