diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-04-28 02:13:30 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-04-28 02:13:30 +0000 |
| commit | ef4c533ebacc2cdc97e518f30e9a9350004fcdfb (patch) | |
| tree | 345251a3ed0f4429716fa5edaa31024d8f4cb560 /components/form-data/publish-dialog.tsx | |
| parent | 9ceed79cf32c896f8a998399bf1b296506b2cd4a (diff) | |
~20250428 작업사항
Diffstat (limited to 'components/form-data/publish-dialog.tsx')
| -rw-r--r-- | components/form-data/publish-dialog.tsx | 470 |
1 files changed, 470 insertions, 0 deletions
diff --git a/components/form-data/publish-dialog.tsx b/components/form-data/publish-dialog.tsx new file mode 100644 index 00000000..a3a2ef0b --- /dev/null +++ b/components/form-data/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 |
