summaryrefslogtreecommitdiff
path: root/components/pq/pq-input-tabs.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'components/pq/pq-input-tabs.tsx')
-rw-r--r--components/pq/pq-input-tabs.tsx884
1 files changed, 0 insertions, 884 deletions
diff --git a/components/pq/pq-input-tabs.tsx b/components/pq/pq-input-tabs.tsx
deleted file mode 100644
index d72eff92..00000000
--- a/components/pq/pq-input-tabs.tsx
+++ /dev/null
@@ -1,884 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { useForm } from "react-hook-form"
-import { zodResolver } from "@hookform/resolvers/zod"
-import { z } from "zod"
-import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
-import {
- Card,
- CardHeader,
- CardTitle,
- CardDescription,
- CardContent,
-} from "@/components/ui/card"
-import { Button } from "@/components/ui/button"
-import { Textarea } from "@/components/ui/textarea"
-import { X, Save, CheckCircle2, AlertTriangle, ChevronsUpDown } from "lucide-react"
-import prettyBytes from "pretty-bytes"
-import { useToast } from "@/hooks/use-toast"
-import {
- Collapsible,
- CollapsibleContent,
- CollapsibleTrigger,
-} from "@/components/ui/collapsible"
-
-// Form components
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
- FormDescription,
-} from "@/components/ui/form"
-
-// Custom Dropzone, FileList components
-import {
- Dropzone,
- DropzoneDescription,
- DropzoneInput,
- DropzoneTitle,
- DropzoneUploadIcon,
- DropzoneZone,
-} from "@/components/ui/dropzone"
-import {
- FileList,
- FileListAction,
- FileListDescription,
- FileListHeader,
- FileListIcon,
- FileListInfo,
- FileListItem,
- FileListName,
-} from "@/components/ui/file-list"
-
-// Dialog components
-import {
- Dialog,
- DialogContent,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogDescription,
-} from "@/components/ui/dialog"
-
-// Additional UI
-import { Separator } from "@/components/ui/separator"
-import { Badge } from "@/components/ui/badge"
-
-// Server actions
-import {
- uploadFileAction,
- savePQAnswersAction,
- submitPQAction,
- ProjectPQ,
-} from "@/lib/pq/service"
-import { PQGroupData } from "@/lib/pq/service"
-import { useRouter } from "next/navigation"
-
-// ----------------------------------------------------------------------
-// 1) Define client-side file shapes
-// ----------------------------------------------------------------------
-interface UploadedFileState {
- fileName: string
- url: string
- size?: number
-}
-
-interface LocalFileState {
- fileObj: File
- uploaded: boolean
-}
-
-// ----------------------------------------------------------------------
-// 2) Zod schema for the entire form
-// ----------------------------------------------------------------------
-const pqFormSchema = z.object({
- answers: z.array(
- z.object({
- criteriaId: z.number(),
- // Must have at least 1 char
- answer: z.string().min(1, "Answer is required"),
-
- // Existing, uploaded files
- uploadedFiles: z
- .array(
- z.object({
- fileName: z.string(),
- url: z.string(),
- size: z.number().optional(),
- })
- )
- .min(1, "At least one file attachment is required"),
-
- // Local (not-yet-uploaded) files
- newUploads: z.array(
- z.object({
- fileObj: z.any(),
- uploaded: z.boolean().default(false),
- })
- ),
-
- // track saved state
- saved: z.boolean().default(false),
- })
- ),
-})
-
-type PQFormValues = z.infer<typeof pqFormSchema>
-
-// ----------------------------------------------------------------------
-// 3) Main Component: PQInputTabs
-// ----------------------------------------------------------------------
-export function PQInputTabs({
- data,
- vendorId,
- projectId,
- projectData,
-}: {
- data: PQGroupData[]
- vendorId: number
- projectId?: number
- projectData?: ProjectPQ | null
-}) {
- const [isSaving, setIsSaving] = React.useState(false)
- const [isSubmitting, setIsSubmitting] = React.useState(false)
- const [allSaved, setAllSaved] = React.useState(false)
- const [showConfirmDialog, setShowConfirmDialog] = React.useState(false)
-
- const { toast } = useToast()
- const router = useRouter()
- // ----------------------------------------------------------------------
- // A) Create initial form values
- // Mark items as "saved" if they have existing answer or attachments
- // ----------------------------------------------------------------------
- function createInitialFormValues(): PQFormValues {
- const answers: PQFormValues["answers"] = []
-
- data.forEach((group) => {
- group.items.forEach((item) => {
- // Check if the server item is already "complete"
- const hasExistingAnswer = item.answer && item.answer.trim().length > 0
- const hasExistingAttachments = item.attachments && item.attachments.length > 0
-
- // If either is present, we consider it "saved" initially
- const isAlreadySaved = hasExistingAnswer || hasExistingAttachments
-
- answers.push({
- criteriaId: item.criteriaId,
- answer: item.answer || "",
- uploadedFiles: item.attachments.map((attach) => ({
- fileName: attach.fileName,
- url: attach.filePath,
- size: attach.fileSize,
- })),
- newUploads: [],
- saved: isAlreadySaved,
- })
- })
- })
-
- return { answers }
- }
-
- // ----------------------------------------------------------------------
- // B) Set up react-hook-form
- // ----------------------------------------------------------------------
- const form = useForm<PQFormValues>({
- resolver: zodResolver(pqFormSchema),
- defaultValues: createInitialFormValues(),
- mode: "onChange",
- })
-
- // ----------------------------------------------------------------------
- // C) Track if all items are saved => controls Submit PQ button
- // ----------------------------------------------------------------------
- React.useEffect(() => {
- const values = form.getValues()
- // We consider items "saved" if `saved===true` AND they have an answer or attachments
- const allItemsSaved = values.answers.every(
- (answer) => answer.saved && (answer.answer || answer.uploadedFiles.length > 0)
- )
- setAllSaved(allItemsSaved)
- }, [form.watch()])
-
- // Helper to find the array index by criteriaId
- const getAnswerIndex = (criteriaId: number): number => {
- return form.getValues().answers.findIndex((a) => a.criteriaId === criteriaId)
- }
-
- // ----------------------------------------------------------------------
- // D) Handling File Drops, Removal
- // ----------------------------------------------------------------------
- const handleDropAccepted = (criteriaId: number, files: File[]) => {
- const answerIndex = getAnswerIndex(criteriaId)
- if (answerIndex === -1) return
-
- // Convert each dropped file into a LocalFileState
- const newLocalFiles: LocalFileState[] = files.map((f) => ({
- fileObj: f,
- uploaded: false,
- }))
-
- const current = form.getValues(`answers.${answerIndex}.newUploads`)
- form.setValue(`answers.${answerIndex}.newUploads`, [...current, ...newLocalFiles], {
- shouldDirty: true,
- })
-
- // Mark unsaved
- form.setValue(`answers.${answerIndex}.saved`, false, { shouldDirty: true })
- }
-
- const handleDropRejected = () => {
- toast({
- title: "File upload rejected",
- description: "Please check file size and type.",
- variant: "destructive",
- })
- }
-
- const removeNewUpload = (answerIndex: number, fileIndex: number) => {
- const current = [...form.getValues(`answers.${answerIndex}.newUploads`)]
- current.splice(fileIndex, 1)
- form.setValue(`answers.${answerIndex}.newUploads`, current, { shouldDirty: true })
-
- form.setValue(`answers.${answerIndex}.saved`, false, { shouldDirty: true })
- }
-
- const removeUploadedFile = (answerIndex: number, fileIndex: number) => {
- const current = [...form.getValues(`answers.${answerIndex}.uploadedFiles`)]
- current.splice(fileIndex, 1)
- form.setValue(`answers.${answerIndex}.uploadedFiles`, current, { shouldDirty: true })
-
- form.setValue(`answers.${answerIndex}.saved`, false, { shouldDirty: true })
- }
-
- // ----------------------------------------------------------------------
- // E) Saving a Single Item
- // ----------------------------------------------------------------------
- const handleSaveItem = async (answerIndex: number) => {
- try {
- const answerData = form.getValues(`answers.${answerIndex}`)
-
- // Validation
- if (!answerData.answer) {
- toast({
- title: "Validation Error",
- description: "Answer is required",
- variant: "destructive",
- })
- return
- }
-
- // Upload new files (if any)
- if (answerData.newUploads.length > 0) {
- setIsSaving(true)
-
- for (const localFile of answerData.newUploads) {
- try {
- const uploadResult = await uploadFileAction(localFile.fileObj)
- const currentUploaded = form.getValues(`answers.${answerIndex}.uploadedFiles`)
- currentUploaded.push({
- fileName: uploadResult.fileName,
- url: uploadResult.url,
- size: uploadResult.size,
- })
- form.setValue(`answers.${answerIndex}.uploadedFiles`, currentUploaded, {
- shouldDirty: true,
- })
- } catch (error) {
- console.error("File upload error:", error)
- toast({
- title: "Upload Error",
- description: "Failed to upload file",
- variant: "destructive",
- })
- }
- }
-
- // Clear newUploads
- form.setValue(`answers.${answerIndex}.newUploads`, [], { shouldDirty: true })
- }
-
- // Save to DB
- const updatedAnswer = form.getValues(`answers.${answerIndex}`)
- const saveResult = await savePQAnswersAction({
- vendorId,
- projectId, // 프로젝트 ID 전달
- answers: [
- {
- criteriaId: updatedAnswer.criteriaId,
- answer: updatedAnswer.answer,
- attachments: updatedAnswer.uploadedFiles.map((f) => ({
- fileName: f.fileName,
- url: f.url,
- size: f.size,
- })),
- },
- ],
- })
-
- if (saveResult.ok) {
- // Mark as saved
- form.setValue(`answers.${answerIndex}.saved`, true, { shouldDirty: false })
- toast({
- title: "Saved",
- description: "Item saved successfully",
- })
- }
- } catch (error) {
- console.error("Save error:", error)
- toast({
- title: "Save Error",
- description: "Failed to save item",
- variant: "destructive",
- })
- } finally {
- setIsSaving(false)
- }
- }
-
- // For convenience
- const answers = form.getValues().answers
- const dirtyFields = form.formState.dirtyFields.answers
-
- // Check if any item is dirty or has new uploads
- const isAnyItemDirty = answers.some((answer, i) => {
- const itemDirty = !!dirtyFields?.[i]
- const hasNewUploads = answer.newUploads.length > 0
- return itemDirty || hasNewUploads
- })
-
- // ----------------------------------------------------------------------
- // F) Save All Items
- // ----------------------------------------------------------------------
- const handleSaveAll = async () => {
- try {
- setIsSaving(true)
- const answers = form.getValues().answers
-
- // Only save items that are dirty or have new uploads
- for (let i = 0; i < answers.length; i++) {
- const itemDirty = !!dirtyFields?.[i]
- const hasNewUploads = answers[i].newUploads.length > 0
- if (!itemDirty && !hasNewUploads) continue
-
- await handleSaveItem(i)
- }
-
- toast({
- title: "All Saved",
- description: "All items saved successfully",
- })
- } catch (error) {
- console.error("Save all error:", error)
- toast({
- title: "Save Error",
- description: "Failed to save all items",
- variant: "destructive",
- })
- } finally {
- setIsSaving(false)
- }
- }
-
- // ----------------------------------------------------------------------
- // G) Submission with Confirmation Dialog
- // ----------------------------------------------------------------------
- const handleSubmitPQ = () => {
- if (!allSaved) {
- toast({
- title: "Cannot Submit",
- description: "Please save all items before submitting",
- variant: "destructive",
- })
- return
- }
- setShowConfirmDialog(true)
- }
-
- const handleConfirmSubmission = async () => {
- try {
- setIsSubmitting(true)
- setShowConfirmDialog(false)
-
- const result = await submitPQAction({
- vendorId,
- projectId, // 프로젝트 ID 전달
- })
-
- if (result.ok) {
- toast({
- title: "PQ Submitted",
- description: "Your PQ information has been submitted successfully",
- })
- // 제출 후 페이지 새로고침 또는 리디렉션 처리
- router.refresh()
- // window.location.reload()
- } else {
- toast({
- title: "Submit Error",
- description: result.error || "Failed to submit PQ",
- variant: "destructive",
- })
- }
- } catch (error) {
- console.error("Submit error:", error)
- toast({
- title: "Submit Error",
- description: "Failed to submit PQ information",
- variant: "destructive",
- })
- } finally {
- setIsSubmitting(false)
- }
- }
-
- // 프로젝트 정보 표시 섹션
- const renderProjectInfo = () => {
- if (!projectData) return null;
-
- return (
- <div className="mb-6 bg-muted p-4 rounded-md">
- <div className="flex items-center justify-between mb-2">
- <h3 className="text-lg font-semibold">프로젝트 정보</h3>
- <Badge variant={getStatusVariant(projectData.status)}>
- {getStatusLabel(projectData.status)}
- </Badge>
- </div>
-
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
- <div>
- <p className="text-sm font-medium text-muted-foreground">프로젝트 코드</p>
- <p>{projectData.projectCode}</p>
- </div>
- <div>
- <p className="text-sm font-medium text-muted-foreground">프로젝트명</p>
- <p>{projectData.projectName}</p>
- </div>
- {projectData.submittedAt && (
- <div className="col-span-1 md:col-span-2">
- <p className="text-sm font-medium text-muted-foreground">제출일</p>
- <p>{formatDate(projectData.submittedAt)}</p>
- </div>
- )}
- </div>
- </div>
- );
- };
-
- // 상태 표시용 함수
- const getStatusLabel = (status: string) => {
- switch (status) {
- case "REQUESTED": return "요청됨";
- case "IN_PROGRESS": return "진행중";
- case "SUBMITTED": return "제출됨";
- case "APPROVED": return "승인됨";
- case "REJECTED": return "반려됨";
- default: return status;
- }
- };
-
- const getStatusVariant = (status: string) => {
- switch (status) {
- case "REQUESTED": return "secondary";
- case "IN_PROGRESS": return "default";
- case "SUBMITTED": return "outline";
- case "APPROVED": return "outline";
- case "REJECTED": return "destructive";
- default: return "secondary";
- }
- };
-
- // 날짜 형식화 함수
- const formatDate = (date: Date) => {
- if (!date) return "-";
- return new Date(date).toLocaleDateString("ko-KR", {
- year: "numeric",
- month: "long",
- day: "numeric",
- });
- };
-
- // ----------------------------------------------------------------------
- // H) Render
- // ----------------------------------------------------------------------
- return (
- <Form {...form}>
- <form>
- {/* 프로젝트 정보 섹션 */}
- {renderProjectInfo()}
-
- <Tabs defaultValue={data[0]?.groupName || ""} className="w-full">
- {/* Top Controls */}
- <div className="flex justify-between items-center mb-4">
- <TabsList className="grid grid-cols-4">
- {data.map((group) => (
- <TabsTrigger
- key={group.groupName}
- value={group.groupName}
- className="truncate"
- >
- <div className="flex items-center gap-2">
- {/* Mobile: truncated version */}
- <span className="block sm:hidden">
- {group.groupName.length > 5
- ? group.groupName.slice(0, 5) + "..."
- : group.groupName}
- </span>
- {/* Desktop: full text */}
- <span className="hidden sm:block">{group.groupName}</span>
- <span className="inline-flex items-center justify-center h-5 min-w-5 px-1 rounded-full bg-muted text-xs font-medium">
- {group.items.length}
- </span>
- </div>
- </TabsTrigger>
- ))}
- </TabsList>
-
- <div className="flex gap-2">
- {/* Save All button */}
- <Button
- type="button"
- variant="outline"
- disabled={isSaving || !isAnyItemDirty}
- onClick={handleSaveAll}
- >
- {isSaving ? "Saving..." : "Save All"}
- <Save className="ml-2 h-4 w-4" />
- </Button>
-
- {/* Submit PQ button */}
- <Button
- type="button"
- disabled={!allSaved || isSubmitting}
- onClick={handleSubmitPQ}
- >
- {isSubmitting ? "Submitting..." : "Submit PQ"}
- <CheckCircle2 className="ml-2 h-4 w-4" />
- </Button>
- </div>
- </div>
-
- {/* Render each group */}
- {data.map((group) => (
- <TabsContent key={group.groupName} value={group.groupName}>
- {/* 2-column grid */}
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4 pb-4">
- {group.items.map((item) => {
- const { criteriaId, code, checkPoint, description, contractInfo, additionalRequirement } = item
- const answerIndex = getAnswerIndex(criteriaId)
- if (answerIndex === -1) return null
-
- const isSaved = form.watch(`answers.${answerIndex}.saved`)
- const hasAnswer = form.watch(`answers.${answerIndex}.answer`)
- const newUploads = form.watch(`answers.${answerIndex}.newUploads`)
- const dirtyFieldsItem = form.formState.dirtyFields.answers?.[answerIndex]
-
- const isItemDirty = !!dirtyFieldsItem
- const hasNewUploads = newUploads.length > 0
- const canSave = isItemDirty || hasNewUploads
-
- // For "Not Saved" vs. "Saved" status label
- const hasUploads =
- form.watch(`answers.${answerIndex}.uploadedFiles`).length > 0 ||
- newUploads.length > 0
- const isValid = !!hasAnswer || hasUploads
-
- return (
- <Collapsible key={criteriaId} defaultOpen={!isSaved} className="w-full">
- <Card className={isSaved ? "border-green-200" : ""}>
- <CardHeader className="pb-1">
- <div className="flex justify-between">
- <div className="flex-1">
- <div className="flex items-center gap-2">
- <CollapsibleTrigger asChild>
- <Button variant="ghost" size="sm" className="p-0 h-7 w-7">
- <ChevronsUpDown className="h-4 w-4" />
- <span className="sr-only">Toggle</span>
- </Button>
- </CollapsibleTrigger>
- <CardTitle className="text-md">
- {code} - {checkPoint}
- </CardTitle>
- </div>
- {description && (
- <CardDescription className="mt-1 whitespace-pre-wrap">
- {description}
- </CardDescription>
- )}
- </div>
-
- {/* Save Status & Button */}
- <div className="flex items-center gap-2">
- {!isSaved && canSave && (
- <span className="text-amber-600 text-xs flex items-center">
- <AlertTriangle className="h-4 w-4 mr-1" />
- Not Saved
- </span>
- )}
- {isSaved && (
- <span className="text-green-600 text-xs flex items-center">
- <CheckCircle2 className="h-4 w-4 mr-1" />
- Saved
- </span>
- )}
-
- <Button
- size="sm"
- variant="outline"
- disabled={isSaving || !canSave}
- onClick={() => handleSaveItem(answerIndex)}
- >
- Save
- </Button>
- </div>
- </div>
- </CardHeader>
-
- <CollapsibleContent>
- <CardContent className="pt-3 space-y-3">
- {/* 프로젝트별 추가 필드 (contractInfo, additionalRequirement) */}
- {projectId && contractInfo && (
- <div className="space-y-1">
- <FormLabel className="text-sm font-medium">계약 정보</FormLabel>
- <div className="rounded-md bg-muted/30 p-3 text-sm whitespace-pre-wrap">
- {contractInfo}
- </div>
- </div>
- )}
-
- {projectId && additionalRequirement && (
- <div className="space-y-1">
- <FormLabel className="text-sm font-medium">추가 요구사항</FormLabel>
- <div className="rounded-md bg-muted/30 p-3 text-sm whitespace-pre-wrap">
- {additionalRequirement}
- </div>
- </div>
- )}
-
- {/* Answer Field */}
- <FormField
- control={form.control}
- name={`answers.${answerIndex}.answer`}
- render={({ field }) => (
- <FormItem className="mt-2">
- <FormLabel>Answer</FormLabel>
- <FormControl>
- <Textarea
- {...field}
- className="min-h-24"
- placeholder="Enter your answer here"
- onChange={(e) => {
- field.onChange(e)
- form.setValue(
- `answers.${answerIndex}.saved`,
- false,
- { shouldDirty: true }
- )
- }}
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
-
- {/* Attachments / Dropzone */}
- <div className="grid gap-2 mt-3">
- <FormLabel>Attachments</FormLabel>
- <Dropzone
- maxSize={6e8} // 600MB
- onDropAccepted={(files) =>
- handleDropAccepted(criteriaId, files)
- }
- onDropRejected={handleDropRejected}
- >
- {() => (
- <FormItem>
- <DropzoneZone className="flex justify-center h-24">
- <FormControl>
- <DropzoneInput />
- </FormControl>
- <div className="flex items-center gap-6">
- <DropzoneUploadIcon />
- <div className="grid gap-0.5">
- <DropzoneTitle>Drop files here</DropzoneTitle>
- <DropzoneDescription>
- Max size: 600MB
- </DropzoneDescription>
- </div>
- </div>
- </DropzoneZone>
- <FormDescription>
- Or click to browse files
- </FormDescription>
- <FormMessage />
- </FormItem>
- )}
- </Dropzone>
- </div>
-
- {/* Existing + Pending Files */}
- <div className="mt-4 space-y-4">
- {/* 1) Not-yet-uploaded files */}
- {newUploads.length > 0 && (
- <div className="grid gap-2">
- <h6 className="text-sm font-medium">
- Pending Files ({newUploads.length})
- </h6>
- <FileList>
- {newUploads.map((f, fileIndex) => {
- const fileObj = f.fileObj
- if (!fileObj) return null
-
- return (
- <FileListItem key={fileIndex}>
- <FileListHeader>
- <FileListIcon />
- <FileListInfo>
- <FileListName>{fileObj.name}</FileListName>
- <FileListDescription>
- {prettyBytes(fileObj.size)}
- </FileListDescription>
- </FileListInfo>
- <FileListAction
- onClick={() =>
- removeNewUpload(answerIndex, fileIndex)
- }
- >
- <X className="h-4 w-4" />
- <span className="sr-only">Remove</span>
- </FileListAction>
- </FileListHeader>
- </FileListItem>
- )
- })}
- </FileList>
- </div>
- )}
-
- {/* 2) Already uploaded files */}
- {form
- .watch(`answers.${answerIndex}.uploadedFiles`)
- .map((file, fileIndex) => (
- <FileListItem key={fileIndex}>
- <FileListHeader>
- <FileListIcon />
- <FileListInfo>
- <FileListName>{file.fileName}</FileListName>
- {/* If you want to display the path:
- <FileListDescription>{file.url}</FileListDescription>
- */}
- </FileListInfo>
- {file.size && (
- <span className="text-xs text-muted-foreground">
- {prettyBytes(file.size)}
- </span>
- )}
- <FileListAction
- onClick={() =>
- removeUploadedFile(answerIndex, fileIndex)
- }
- >
- <X className="h-4 w-4" />
- <span className="sr-only">Remove</span>
- </FileListAction>
- </FileListHeader>
- </FileListItem>
- ))}
- </div>
- </CardContent>
- </CollapsibleContent>
- </Card>
- </Collapsible>
- )
- })}
- </div>
- </TabsContent>
- ))}
- </Tabs>
- </form>
-
- {/* Confirmation Dialog */}
- <Dialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}>
- <DialogContent>
- <DialogHeader>
- <DialogTitle>Confirm Submission</DialogTitle>
- <DialogDescription>
- {projectId
- ? `${projectData?.projectCode} 프로젝트의 PQ 응답을 제출하시겠습니까?`
- : "일반 PQ 응답을 제출하시겠습니까?"
- } 제출 후에는 수정이 불가능합니다.
- </DialogDescription>
- </DialogHeader>
-
- <div className="space-y-4 max-h-[600px] overflow-y-auto ">
- {data.map((group) => (
- <Collapsible key={group.groupName} defaultOpen>
- <CollapsibleTrigger asChild>
- <div className="flex justify-between items-center p-2 mb-1 cursor-pointer ">
- <p className="font-semibold">{group.groupName}</p>
- <ChevronsUpDown className="h-4 w-4 ml-2" />
- </div>
- </CollapsibleTrigger>
-
- <CollapsibleContent>
- {group.items.map((item) => {
- const answerObj = form
- .getValues()
- .answers.find((a) => a.criteriaId === item.criteriaId)
-
- if (!answerObj) return null
-
- return (
- <div key={item.criteriaId} className="mb-2 p-2 ml-2 border rounded-md text-sm">
- {/* code & checkPoint */}
- <p className="font-semibold">
- {item.code} - {item.checkPoint}
- </p>
-
- {/* user's typed answer */}
- <p className="text-sm font-medium mt-2">Answer:</p>
- <p className="whitespace-pre-wrap text-sm">
- {answerObj.answer || "(no answer)"}
- </p>
- {/* attachments */}
- <p>Attachments:</p>
- {answerObj.uploadedFiles.length > 0 ? (
- <ul className="list-disc list-inside ml-4 text-xs">
- {answerObj.uploadedFiles.map((file, idx) => (
- <li key={idx}>{file.fileName}</li>
- ))}
- </ul>
- ) : (
- <p className="text-xs text-muted-foreground">(none)</p>
- )}
- </div>
- )
- })}
- </CollapsibleContent>
- </Collapsible>
- ))}
- </div>
-
- <DialogFooter>
- <Button
- variant="outline"
- onClick={() => setShowConfirmDialog(false)}
- disabled={isSubmitting}
- >
- Cancel
- </Button>
- <Button onClick={handleConfirmSubmission} disabled={isSubmitting}>
- {isSubmitting ? "Submitting..." : "Confirm Submit"}
- </Button>
- </DialogFooter>
- </DialogContent>
- </Dialog>
- </Form>
- )
-} \ No newline at end of file