diff options
Diffstat (limited to 'lib/vendor-rfq-response/vendor-cbe-table/respond-cbe-sheet.tsx')
| -rw-r--r-- | lib/vendor-rfq-response/vendor-cbe-table/respond-cbe-sheet.tsx | 427 |
1 files changed, 427 insertions, 0 deletions
diff --git a/lib/vendor-rfq-response/vendor-cbe-table/respond-cbe-sheet.tsx b/lib/vendor-rfq-response/vendor-cbe-table/respond-cbe-sheet.tsx new file mode 100644 index 00000000..8cc4fa6f --- /dev/null +++ b/lib/vendor-rfq-response/vendor-cbe-table/respond-cbe-sheet.tsx @@ -0,0 +1,427 @@ +"use client" + +import * as React from "react" +import { zodResolver } from "@hookform/resolvers/zod" +import { Loader } from "lucide-react" +import { useForm } from "react-hook-form" +import { toast } from "sonner" +import { z } from "zod" + +import { Button } from "@/components/ui/button" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { Input } from "@/components/ui/input" +import { + Sheet, + SheetClose, + SheetContent, + SheetDescription, + SheetFooter, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet" +import { Textarea } from "@/components/ui/textarea" +import { VendorWithCbeFields } from "@/config/vendorCbeColumnsConfig" +import { getCommercialResponseByResponseId, updateCommercialResponse } from "../service" + +// Define schema for form validation (client-side) +const commercialResponseFormSchema = z.object({ + responseStatus: z.enum(["PENDING", "IN_PROGRESS", "SUBMITTED", "REJECTED", "ACCEPTED"]), + totalPrice: z.coerce.number().optional(), + currency: z.string().default("USD"), + paymentTerms: z.string().optional(), + incoterms: z.string().optional(), + deliveryPeriod: z.string().optional(), + warrantyPeriod: z.string().optional(), + validityPeriod: z.string().optional(), + priceBreakdown: z.string().optional(), + commercialNotes: z.string().optional(), +}) + +type CommercialResponseFormInput = z.infer<typeof commercialResponseFormSchema> + +interface CommercialResponseSheetProps + extends React.ComponentPropsWithRef<typeof Sheet> { + rfq: VendorWithCbeFields | null + responseId: number | null // This is the vendor_responses.id + onSuccess?: () => void +} + +export function CommercialResponseSheet({ + rfq, + responseId, + onSuccess, + ...props +}: CommercialResponseSheetProps) { + const [isSubmitting, startSubmitTransition] = React.useTransition() + const [isLoading, setIsLoading] = React.useState(true) + + const form = useForm<CommercialResponseFormInput>({ + resolver: zodResolver(commercialResponseFormSchema), + defaultValues: { + responseStatus: "PENDING", + totalPrice: undefined, + currency: "USD", + paymentTerms: "", + incoterms: "", + deliveryPeriod: "", + warrantyPeriod: "", + validityPeriod: "", + priceBreakdown: "", + commercialNotes: "", + }, + }) + + // Load existing commercial response data when sheet opens + React.useEffect(() => { + async function loadCommercialResponse() { + if (!responseId) return + + setIsLoading(true) + try { + // Use the helper function to get existing data + const existingResponse = await getCommercialResponseByResponseId(responseId) + + if (existingResponse) { + // If we found existing data, populate the form + form.reset({ + responseStatus: existingResponse.responseStatus, + totalPrice: existingResponse.totalPrice, + currency: existingResponse.currency || "USD", + paymentTerms: existingResponse.paymentTerms || "", + incoterms: existingResponse.incoterms || "", + deliveryPeriod: existingResponse.deliveryPeriod || "", + warrantyPeriod: existingResponse.warrantyPeriod || "", + validityPeriod: existingResponse.validityPeriod || "", + priceBreakdown: existingResponse.priceBreakdown || "", + commercialNotes: existingResponse.commercialNotes || "", + }) + } else if (rfq) { + // If no existing data but we have rfq data with some values already + form.reset({ + responseStatus: rfq.commercialResponseStatus as any || "PENDING", + totalPrice: rfq.totalPrice || undefined, + currency: rfq.currency || "USD", + paymentTerms: rfq.paymentTerms || "", + incoterms: rfq.incoterms || "", + deliveryPeriod: rfq.deliveryPeriod || "", + warrantyPeriod: rfq.warrantyPeriod || "", + validityPeriod: rfq.validityPeriod || "", + priceBreakdown: "", + commercialNotes: "", + }) + } + } catch (error) { + console.error("Failed to load commercial response data:", error) + toast.error("상업 응답 데이터를 불러오는데 실패했습니다") + } finally { + setIsLoading(false) + } + } + + loadCommercialResponse() + }, [responseId, rfq, form]) + + function onSubmit(formData: CommercialResponseFormInput) { + if (!responseId) { + toast.error("응답 ID를 찾을 수 없습니다") + return + } + + if (!rfq?.vendorId) { + toast.error("협력업체 ID를 찾을 수 없습니다") + return + } + + startSubmitTransition(async () => { + try { + // Pass both responseId and vendorId to the server action + const result = await updateCommercialResponse({ + responseId, + vendorId: rfq.vendorId, // Include vendorId for revalidateTag + ...formData, + }) + + if (!result.success) { + toast.error(result.error || "응답 제출 중 오류가 발생했습니다") + return + } + + toast.success("Commercial response successfully submitted") + props.onOpenChange?.(false) + + if (onSuccess) { + onSuccess() + } + } catch (error) { + console.error("Error submitting response:", error) + toast.error("응답 제출 중 오류가 발생했습니다") + } + }) + } + + return ( + <Sheet {...props}> + <SheetContent className="flex flex-col gap-6 sm:max-w-md"> + <SheetHeader className="text-left"> + <SheetTitle>Commercial Response</SheetTitle> + <SheetDescription> + {rfq?.rfqCode && <span className="font-medium">{rfq.rfqCode}</span>} + <div className="mt-1">Please provide your commercial response for this RFQ</div> + </SheetDescription> + </SheetHeader> + + {isLoading ? ( + <div className="flex items-center justify-center py-8"> + <Loader className="h-8 w-8 animate-spin text-muted-foreground" /> + </div> + ) : ( + <Form {...form}> + <form + onSubmit={form.handleSubmit(onSubmit)} + className="flex flex-col gap-4 overflow-y-auto max-h-[calc(100vh-200px)] pr-2" + > + <FormField + control={form.control} + name="responseStatus" + render={({ field }) => ( + <FormItem> + <FormLabel>Response Status</FormLabel> + <Select + onValueChange={field.onChange} + defaultValue={field.value} + > + <FormControl> + <SelectTrigger className="capitalize"> + <SelectValue placeholder="Select response status" /> + </SelectTrigger> + </FormControl> + <SelectContent> + <SelectGroup> + <SelectItem value="PENDING">Pending</SelectItem> + <SelectItem value="IN_PROGRESS">In Progress</SelectItem> + <SelectItem value="SUBMITTED">Submitted</SelectItem> + </SelectGroup> + </SelectContent> + </Select> + <FormMessage /> + </FormItem> + )} + /> + + <div className="grid grid-cols-2 gap-4"> + <FormField + control={form.control} + name="totalPrice" + render={({ field }) => ( + <FormItem> + <FormLabel>Total Price</FormLabel> + <FormControl> + <Input + type="number" + placeholder="0.00" + {...field} + value={field.value || ''} + onChange={(e) => { + const value = e.target.value === '' ? undefined : parseFloat(e.target.value); + field.onChange(value); + }} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="currency" + render={({ field }) => ( + <FormItem> + <FormLabel>Currency</FormLabel> + <Select + onValueChange={field.onChange} + defaultValue={field.value} + > + <FormControl> + <SelectTrigger> + <SelectValue placeholder="Select currency" /> + </SelectTrigger> + </FormControl> + <SelectContent> + <SelectGroup> + <SelectItem value="USD">USD</SelectItem> + <SelectItem value="EUR">EUR</SelectItem> + <SelectItem value="GBP">GBP</SelectItem> + <SelectItem value="KRW">KRW</SelectItem> + <SelectItem value="JPY">JPY</SelectItem> + </SelectGroup> + </SelectContent> + </Select> + <FormMessage /> + </FormItem> + )} + /> + </div> + + {/* Other form fields remain the same */} + <FormField + control={form.control} + name="paymentTerms" + render={({ field }) => ( + <FormItem> + <FormLabel>Payment Terms</FormLabel> + <FormControl> + <Input placeholder="e.g. Net 30" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="incoterms" + render={({ field }) => ( + <FormItem> + <FormLabel>Incoterms</FormLabel> + <Select + onValueChange={field.onChange} + defaultValue={field.value || ''} + > + <FormControl> + <SelectTrigger> + <SelectValue placeholder="Select incoterms" /> + </SelectTrigger> + </FormControl> + <SelectContent> + <SelectGroup> + <SelectItem value="EXW">EXW (Ex Works)</SelectItem> + <SelectItem value="FCA">FCA (Free Carrier)</SelectItem> + <SelectItem value="FOB">FOB (Free On Board)</SelectItem> + <SelectItem value="CIF">CIF (Cost, Insurance & Freight)</SelectItem> + <SelectItem value="DAP">DAP (Delivered At Place)</SelectItem> + <SelectItem value="DDP">DDP (Delivered Duty Paid)</SelectItem> + </SelectGroup> + </SelectContent> + </Select> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="deliveryPeriod" + render={({ field }) => ( + <FormItem> + <FormLabel>Delivery Period</FormLabel> + <FormControl> + <Input placeholder="e.g. 4-6 weeks" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="warrantyPeriod" + render={({ field }) => ( + <FormItem> + <FormLabel>Warranty Period</FormLabel> + <FormControl> + <Input placeholder="e.g. 12 months" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="validityPeriod" + render={({ field }) => ( + <FormItem> + <FormLabel>Validity Period</FormLabel> + <FormControl> + <Input placeholder="e.g. 30 days" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="priceBreakdown" + render={({ field }) => ( + <FormItem> + <FormLabel>Price Breakdown (Optional)</FormLabel> + <FormControl> + <Textarea + placeholder="Enter price breakdown details here" + className="min-h-[100px]" + {...field} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="commercialNotes" + render={({ field }) => ( + <FormItem> + <FormLabel>Additional Notes (Optional)</FormLabel> + <FormControl> + <Textarea + placeholder="Any additional comments or notes" + className="min-h-[100px]" + {...field} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + <SheetFooter className="gap-2 pt-4 sm:space-x-0"> + <SheetClose asChild> + <Button type="button" variant="outline"> + Cancel + </Button> + </SheetClose> + <Button disabled={isSubmitting} type="submit"> + {isSubmitting && ( + <Loader + className="mr-2 size-4 animate-spin" + aria-hidden="true" + /> + )} + Submit Response + </Button> + </SheetFooter> + </form> + </Form> + )} + </SheetContent> + </Sheet> + ) +}
\ No newline at end of file |
