diff options
| author | joonhoekim <26rote@gmail.com> | 2025-03-25 15:55:45 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-03-25 15:55:45 +0900 |
| commit | 1a2241c40e10193c5ff7008a7b7b36cc1d855d96 (patch) | |
| tree | 8a5587f10ca55b162d7e3254cb088b323a34c41b /lib/po/table | |
initial commit
Diffstat (limited to 'lib/po/table')
| -rw-r--r-- | lib/po/table/feature-flags-provider.tsx | 108 | ||||
| -rw-r--r-- | lib/po/table/po-table-columns.tsx | 155 | ||||
| -rw-r--r-- | lib/po/table/po-table-toolbar-actions.tsx | 53 | ||||
| -rw-r--r-- | lib/po/table/po-table.tsx | 164 | ||||
| -rw-r--r-- | lib/po/table/sign-request-dialog.tsx | 410 |
5 files changed, 890 insertions, 0 deletions
diff --git a/lib/po/table/feature-flags-provider.tsx b/lib/po/table/feature-flags-provider.tsx new file mode 100644 index 00000000..81131894 --- /dev/null +++ b/lib/po/table/feature-flags-provider.tsx @@ -0,0 +1,108 @@ +"use client" + +import * as React from "react" +import { useQueryState } from "nuqs" + +import { dataTableConfig, type DataTableConfig } from "@/config/data-table" +import { cn } from "@/lib/utils" +import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group" +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip" + +type FeatureFlagValue = DataTableConfig["featureFlags"][number]["value"] + +interface FeatureFlagsContextProps { + featureFlags: FeatureFlagValue[] + setFeatureFlags: (value: FeatureFlagValue[]) => void +} + +const FeatureFlagsContext = React.createContext<FeatureFlagsContextProps>({ + featureFlags: [], + setFeatureFlags: () => {}, +}) + +export function useFeatureFlags() { + const context = React.useContext(FeatureFlagsContext) + if (!context) { + throw new Error( + "useFeatureFlags must be used within a FeatureFlagsProvider" + ) + } + return context +} + +interface FeatureFlagsProviderProps { + children: React.ReactNode +} + +export function FeatureFlagsProvider({ children }: FeatureFlagsProviderProps) { + const [featureFlags, setFeatureFlags] = useQueryState<FeatureFlagValue[]>( + "flags", + { + defaultValue: [], + parse: (value) => value.split(",") as FeatureFlagValue[], + serialize: (value) => value.join(","), + eq: (a, b) => + a.length === b.length && a.every((value, index) => value === b[index]), + clearOnDefault: true, + shallow: false, + } + ) + + return ( + <FeatureFlagsContext.Provider + value={{ + featureFlags, + setFeatureFlags: (value) => void setFeatureFlags(value), + }} + > + <div className="w-full overflow-x-auto"> + <ToggleGroup + type="multiple" + variant="outline" + size="sm" + value={featureFlags} + onValueChange={(value: FeatureFlagValue[]) => setFeatureFlags(value)} + className="w-fit gap-0" + > + {dataTableConfig.featureFlags.map((flag, index) => ( + <Tooltip key={flag.value}> + <ToggleGroupItem + value={flag.value} + className={cn( + "gap-2 whitespace-nowrap rounded-none px-3 text-xs data-[state=on]:bg-accent/70 data-[state=on]:hover:bg-accent/90", + { + "rounded-l-sm border-r-0": index === 0, + "rounded-r-sm": + index === dataTableConfig.featureFlags.length - 1, + } + )} + asChild + > + <TooltipTrigger> + <flag.icon className="size-3.5 shrink-0" aria-hidden="true" /> + {flag.label} + </TooltipTrigger> + </ToggleGroupItem> + <TooltipContent + align="start" + side="bottom" + sideOffset={6} + className="flex max-w-60 flex-col space-y-1.5 border bg-background py-2 font-semibold text-foreground" + > + <div>{flag.tooltipTitle}</div> + <div className="text-xs text-muted-foreground"> + {flag.tooltipDescription} + </div> + </TooltipContent> + </Tooltip> + ))} + </ToggleGroup> + </div> + {children} + </FeatureFlagsContext.Provider> + ) +} diff --git a/lib/po/table/po-table-columns.tsx b/lib/po/table/po-table-columns.tsx new file mode 100644 index 00000000..c2c01136 --- /dev/null +++ b/lib/po/table/po-table-columns.tsx @@ -0,0 +1,155 @@ +"use client" + +import * as React from "react" +import { type DataTableRowAction } from "@/types/table" +import { type ColumnDef } from "@tanstack/react-table" +import { InfoIcon, PenIcon } from "lucide-react" + +import { formatDate } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" + +import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" +import { poColumnsConfig } from "@/config/poColumnsConfig" +import { ContractDetail } from "@/db/schema/contract" + +interface GetColumnsProps { + setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<ContractDetail> | null>> +} + +/** + * tanstack table column definitions with nested headers + */ +export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<ContractDetail>[] { + // ---------------------------------------------------------------- + // 1) select column (checkbox) - if needed + // ---------------------------------------------------------------- + + // ---------------------------------------------------------------- + // 2) actions column (buttons for item info and signature request) + // ---------------------------------------------------------------- + const actionsColumn: ColumnDef<ContractDetail> = { + id: "actions", + enableHiding: false, + cell: function Cell({ row }) { + // Check if this contract already has a signature envelope + const hasSignature = row.original.hasSignature; + + return ( + <div className="flex items-center space-x-1"> + {/* Item Info Button */} + <TooltipProvider> + <Tooltip> + <TooltipTrigger asChild> + <Button + variant="ghost" + size="icon" + onClick={() => setRowAction({ row, type: "items" })} + > + <InfoIcon className="h-4 w-4" aria-hidden="true" /> + </Button> + </TooltipTrigger> + <TooltipContent> + View Item Info + </TooltipContent> + </Tooltip> + </TooltipProvider> + + {/* Signature Request Button - only show if no signature exists */} + {!hasSignature && ( + <TooltipProvider> + <Tooltip> + <TooltipTrigger asChild> + <Button + variant="ghost" + size="icon" + onClick={() => setRowAction({ row, type: "signature" })} + > + <PenIcon className="h-4 w-4" aria-hidden="true" /> + </Button> + </TooltipTrigger> + <TooltipContent> + Request Electronic Signature + </TooltipContent> + </Tooltip> + </TooltipProvider> + )} + </div> + ); + }, + size: 80, // Increased width to accommodate both buttons + }; + + // ---------------------------------------------------------------- + // 3) Regular columns grouped by group name + // ---------------------------------------------------------------- + // 3-1) groupMap: { [groupName]: ColumnDef<ContractDetail>[] } + const groupMap: Record<string, ColumnDef<ContractDetail>[]> = {}; + + poColumnsConfig.forEach((cfg) => { + // Use "_noGroup" if no group is specified + const groupName = cfg.group || "_noGroup"; + + if (!groupMap[groupName]) { + groupMap[groupName] = []; + } + + // Child column definition + const childCol: ColumnDef<ContractDetail> = { + accessorKey: cfg.id, + enableResizing: true, + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title={cfg.label} /> + ), + meta: { + excelHeader: cfg.excelHeader, + group: cfg.group, + type: cfg.type, + }, + cell: ({ row, cell }) => { + if (cfg.id === "createdAt" || cfg.id === "updatedAt") { + const dateVal = cell.getValue() as Date; + return formatDate(dateVal); + } + + return row.getValue(cfg.id) ?? ""; + }, + }; + + groupMap[groupName].push(childCol); + }); + + // ---------------------------------------------------------------- + // 3-2) Create actual parent columns (groups) from the groupMap + // ---------------------------------------------------------------- + const nestedColumns: ColumnDef<ContractDetail>[] = []; + + // Order can be fixed by pre-defining group order or sorting + // Here we just use Object.entries order + Object.entries(groupMap).forEach(([groupName, colDefs]) => { + if (groupName === "_noGroup") { + // No group → Add as top-level columns + nestedColumns.push(...colDefs); + } else { + // Parent column + nestedColumns.push({ + id: groupName, + header: groupName, // "Basic Info", "Metadata", etc. + columns: colDefs, + }); + } + }); + + // ---------------------------------------------------------------- + // 4) Final column array: nestedColumns + actionsColumn + // ---------------------------------------------------------------- + return [ + ...nestedColumns, + actionsColumn, + ]; +}
\ No newline at end of file diff --git a/lib/po/table/po-table-toolbar-actions.tsx b/lib/po/table/po-table-toolbar-actions.tsx new file mode 100644 index 00000000..e6c8e79a --- /dev/null +++ b/lib/po/table/po-table-toolbar-actions.tsx @@ -0,0 +1,53 @@ +"use client" + +import * as React from "react" +import { type Table } from "@tanstack/react-table" +import { Download, RefreshCcw, Upload } from "lucide-react" +import { toast } from "sonner" + +import { exportTableToExcel } from "@/lib/export" +import { Button } from "@/components/ui/button" +import { ContractDetail } from "@/db/schema/contract" + + + +interface ItemsTableToolbarActionsProps { + table: Table<ContractDetail> +} + +export function PoTableToolbarActions({ table }: ItemsTableToolbarActionsProps) { + // 파일 input을 숨기고, 버튼 클릭 시 참조해 클릭하는 방식 + const fileInputRef = React.useRef<HTMLInputElement>(null) + + + + return ( + <div className="flex items-center gap-2"> + {/** 4) Export 버튼 */} + <Button + variant="samsung" + size="sm" + className="gap-2" + > + <RefreshCcw className="size-4" aria-hidden="true" /> + <span className="hidden sm:inline">Get POs</span> + </Button> + + {/** 4) Export 버튼 */} + <Button + variant="outline" + size="sm" + onClick={() => + exportTableToExcel(table, { + filename: "tasks", + excludeColumns: ["select", "actions"], + }) + } + className="gap-2" + > + <Download className="size-4" aria-hidden="true" /> + <span className="hidden sm:inline">Export</span> + </Button> + </div> + ) +}
\ No newline at end of file diff --git a/lib/po/table/po-table.tsx b/lib/po/table/po-table.tsx new file mode 100644 index 00000000..49fbdda4 --- /dev/null +++ b/lib/po/table/po-table.tsx @@ -0,0 +1,164 @@ +"use client" + +import * as React from "react" +import type { + DataTableAdvancedFilterField, + DataTableFilterField, + DataTableRowAction, +} from "@/types/table" + +import { useDataTable } from "@/hooks/use-data-table" +import { DataTable } from "@/components/data-table/data-table" +import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar" +import { useFeatureFlags } from "./feature-flags-provider" +import { toast } from "sonner" + +import { getPOs, requestSignatures } from "../service" +import { getColumns } from "./po-table-columns" +import { ContractDetail } from "@/db/schema/contract" +import { PoTableToolbarActions } from "./po-table-toolbar-actions" +import { SignatureRequestModal } from "./sign-request-dialog" + +interface ItemsTableProps { + promises: Promise< + [ + Awaited<ReturnType<typeof getPOs>>, + ] + > +} + +// Interface for signing party +interface SigningParty { + signerEmail: string; + signerName: string; + signerPosition: string; + signerType: "REQUESTER" | "VENDOR"; + vendorContactId?: number; +} + +export function PoListsTable({ promises }: ItemsTableProps) { + const { featureFlags } = useFeatureFlags() + + const [{ data, pageCount }] = + React.use(promises) + + const [rowAction, setRowAction] = + React.useState<DataTableRowAction<ContractDetail> | null>(null) + + // State for signature request modal + const [signatureModalOpen, setSignatureModalOpen] = React.useState(false) + const [selectedContract, setSelectedContract] = React.useState<ContractDetail | null>(null) + + // Handle row actions + React.useEffect(() => { + if (!rowAction) return + + if (rowAction.type === "signature") { + // Open signature request modal with the selected contract + setSelectedContract(rowAction.row.original) + setSignatureModalOpen(true) + setRowAction(null) + } else if (rowAction.type === "items") { + // Existing handler for "items" action type + // Your existing code here + setRowAction(null) + } + }, [rowAction]) + + const columns = React.useMemo( + () => getColumns({ setRowAction }), + [setRowAction] + ) + + // Updated handler to work with multiple signers + const handleSignatureRequest = async ( + values: { signers: SigningParty[] }, + contractId: number + ): Promise<void> => { + try { + const result = await requestSignatures({ + contractId, + signers: values.signers + }); + + // Handle the result + if (result.success) { + toast.success(result.message || "Signature requests sent successfully"); + } else { + toast.error(result.message || "Failed to send signature requests"); + } + } catch (error) { + console.error("Error sending signature requests:", error); + toast.error("An error occurred while sending the signature requests"); + } + } + + const filterFields: DataTableFilterField<ContractDetail>[] = [ + // Your existing filter fields + ] + + const advancedFilterFields: DataTableAdvancedFilterField<ContractDetail>[] = [ + { + id: "contractNo", + label: "Contract No", + type: "text", + }, + { + id: "contractName", + label: "Contract Name", + type: "text", + }, + { + id: "createdAt", + label: "Created At", + type: "date", + }, + { + id: "updatedAt", + label: "Updated At", + type: "date", + }, + ] + + const { table } = useDataTable({ + data, + columns, + pageCount, + filterFields, + enablePinning: true, + enableAdvancedFilter: true, + initialState: { + sorting: [{ id: "createdAt", desc: true }], + columnPinning: { right: ["actions"] }, + }, + getRowId: (originalRow) => String(originalRow.id), + shallow: false, + clearOnDefault: true, + }) + + return ( + <> + <DataTable + table={table} + > + <DataTableAdvancedToolbar + table={table} + filterFields={advancedFilterFields} + shallow={false} + > + <PoTableToolbarActions table={table} /> + </DataTableAdvancedToolbar> + </DataTable> + + {/* Enhanced Dual Signature Request Modal */} + {selectedContract && ( + <SignatureRequestModal + contract={selectedContract} + open={signatureModalOpen} + onOpenChange={setSignatureModalOpen} + onSubmit={handleSignatureRequest} + /> + )} + </> + ) +}
\ No newline at end of file diff --git a/lib/po/table/sign-request-dialog.tsx b/lib/po/table/sign-request-dialog.tsx new file mode 100644 index 00000000..f70e5e33 --- /dev/null +++ b/lib/po/table/sign-request-dialog.tsx @@ -0,0 +1,410 @@ +"use client" + +import { useState, useEffect } from "react" +import { z } from "zod" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import { toast } from "sonner" + +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion" +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { Input } from "@/components/ui/input" +import { Button } from "@/components/ui/button" +import { Checkbox } from "@/components/ui/checkbox" +import { ContractDetail } from "@/db/schema/contract" +import { getVendorContacts } from "../service" + +// Type for vendor contact +interface VendorContact { + id: number + contactName: string + contactEmail: string + contactPosition: string | null + isPrimary: boolean +} + +// Form schema for signature request +const signatureRequestSchema = z.object({ + // Requester signer information + includeRequesterSigner: z.boolean().default(true), + requesterEmail: z.string().email("Please enter a valid email address").optional(), + requesterName: z.string().min(1, "Please enter the signer's name").optional(), + requesterPosition: z.string().optional(), + + // Vendor signer information + includeVendorSigner: z.boolean().default(true), + vendorContactId: z.number().optional(), +}).refine(data => data.includeRequesterSigner || data.includeVendorSigner, { + message: "At least one signer must be included", + path: ["includeRequesterSigner"] +}).refine(data => !data.includeRequesterSigner || (data.requesterEmail && data.requesterName), { + message: "Requester email and name are required", + path: ["requesterEmail"] +}).refine(data => !data.includeVendorSigner || data.vendorContactId, { + message: "Please select a vendor contact", + path: ["vendorContactId"] +}); + +type SignatureRequestFormValues = z.infer<typeof signatureRequestSchema> + +// Interface for signing parties +interface SigningParty { + signerEmail: string; + signerName: string; + signerPosition: string; + signerType: "REQUESTER" | "VENDOR"; + vendorContactId?: number; +} + +// Updated interface to accept multiple signers +interface SignatureRequestModalProps { + contract: ContractDetail + open: boolean + onOpenChange: (open: boolean) => void + onSubmit: ( + values: { + signers: SigningParty[] + }, + contractId: number + ) => Promise<{ success: boolean; message: string } | void> +} + +export function SignatureRequestModal({ + contract, + open, + onOpenChange, + onSubmit, +}: SignatureRequestModalProps) { + const [isSubmitting, setIsSubmitting] = useState(false) + const [vendorContacts, setVendorContacts] = useState<VendorContact[]>([]) + const [selectedVendorContact, setSelectedVendorContact] = useState<VendorContact | null>(null) + + const form = useForm<SignatureRequestFormValues>({ + resolver: zodResolver(signatureRequestSchema), + defaultValues: { + includeRequesterSigner: true, + requesterEmail: "", + requesterName: "", + requesterPosition: "", + includeVendorSigner: true, + vendorContactId: undefined, + }, + }) + + // Load vendor contacts when the modal opens + useEffect(() => { + if (open && contract?.vendorId) { + const loadVendorContacts = async () => { + try { + const contacts = await getVendorContacts(contract.vendorId); + setVendorContacts(contacts); + + // Auto-select primary contact if available + const primaryContact = contacts.find(c => c.isPrimary); + if (primaryContact) { + handleVendorContactSelect(primaryContact.id.toString()); + } + } catch (error) { + console.error("Error loading vendor contacts:", error); + toast.error("Failed to load vendor contacts"); + } + }; + + loadVendorContacts(); + } + }, [open, contract]); + + // Handle selection of a vendor contact + const handleVendorContactSelect = (contactId: string) => { + const id = Number(contactId); + form.setValue("vendorContactId", id); + + // Find the selected contact to show details + const contact = vendorContacts.find(c => c.id === id); + if (contact) { + setSelectedVendorContact(contact); + } + }; + + async function handleSubmit(values: SignatureRequestFormValues) { + setIsSubmitting(true); + + try { + const signers: SigningParty[] = []; + + // Add requester signer if included + if (values.includeRequesterSigner && values.requesterEmail && values.requesterName) { + signers.push({ + signerEmail: values.requesterEmail, + signerName: values.requesterName, + signerPosition: values.requesterPosition || "", + signerType: "REQUESTER" + }); + } + + // Add vendor signer if included + if (values.includeVendorSigner && values.vendorContactId && selectedVendorContact) { + signers.push({ + signerEmail: selectedVendorContact.contactEmail, + signerName: selectedVendorContact.contactName, + signerPosition: selectedVendorContact.contactPosition || "", + vendorContactId: values.vendorContactId, + signerType: "VENDOR" + }); + } + + if (signers.length === 0) { + throw new Error("At least one signer must be included"); + } + + const result = await onSubmit({ signers }, contract.id); + + // Handle the result if it exists + if (result && typeof result === 'object') { + if (result.success) { + toast.success(result.message || "Signature requests sent successfully"); + } else { + toast.error(result.message || "Failed to send signature requests"); + } + } else { + // If no result is returned, assume success + toast.success("Electronic signature requests sent successfully"); + } + + form.reset(); + onOpenChange(false); + } catch (error) { + console.error("Error sending signature requests:", error); + toast.error(error instanceof Error ? error.message : "Failed to send signature requests. Please try again."); + } finally { + setIsSubmitting(false); + } + } + + return ( + <Dialog open={open} onOpenChange={onOpenChange}> + <DialogContent className="sm:max-w-[600px]"> + <DialogHeader> + <DialogTitle>Request Electronic Signatures</DialogTitle> + <DialogDescription> + Send signature requests for contract: {contract?.contractName || ""} + </DialogDescription> + </DialogHeader> + + <Form {...form}> + <form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6"> + <Accordion type="multiple" defaultValue={["requester", "vendor"]} className="w-full"> + {/* Requester Signature Section */} + <AccordionItem value="requester"> + <div className="flex items-center space-x-2"> + <FormField + control={form.control} + name="includeRequesterSigner" + render={({ field }) => ( + <FormItem className="flex flex-row items-center space-x-3 space-y-0 my-2"> + <FormControl> + <Checkbox + checked={field.value} + onCheckedChange={field.onChange} + /> + </FormControl> + <AccordionTrigger className="hover:no-underline ml-2"> + <div className="text-sm font-medium">Requester Signature</div> + </AccordionTrigger> + </FormItem> + )} + /> + </div> + <AccordionContent> + {form.watch("includeRequesterSigner") && ( + <Card className="border-none shadow-none"> + <CardContent className="p-0 space-y-4"> + <FormField + control={form.control} + name="requesterEmail" + render={({ field }) => ( + <FormItem> + <FormLabel>Signer Email</FormLabel> + <FormControl> + <Input placeholder="email@example.com" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="requesterName" + render={({ field }) => ( + <FormItem> + <FormLabel>Signer Name</FormLabel> + <FormControl> + <Input placeholder="Full Name" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="requesterPosition" + render={({ field }) => ( + <FormItem> + <FormLabel>Signer Position</FormLabel> + <FormControl> + <Input placeholder="e.g. CEO, Manager" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + </CardContent> + </Card> + )} + </AccordionContent> + </AccordionItem> + + {/* Vendor Signature Section */} + <AccordionItem value="vendor"> + <div className="flex items-center space-x-2"> + <FormField + control={form.control} + name="includeVendorSigner" + render={({ field }) => ( + <FormItem className="flex flex-row items-center space-x-3 space-y-0 my-2"> + <FormControl> + <Checkbox + checked={field.value} + onCheckedChange={field.onChange} + /> + </FormControl> + <AccordionTrigger className="hover:no-underline ml-2"> + <div className="text-sm font-medium">Vendor Signature</div> + </AccordionTrigger> + </FormItem> + )} + /> + </div> + <AccordionContent> + {form.watch("includeVendorSigner") && ( + <Card className="border-none shadow-none"> + <CardContent className="p-0 space-y-4"> + <FormField + control={form.control} + name="vendorContactId" + render={({ field }) => ( + <FormItem> + <FormLabel>Select Vendor Contact</FormLabel> + <Select + onValueChange={handleVendorContactSelect} + defaultValue={field.value?.toString()} + > + <FormControl> + <SelectTrigger> + <SelectValue placeholder="Select a contact" /> + </SelectTrigger> + </FormControl> + <SelectContent> + {vendorContacts.length > 0 ? ( + vendorContacts.map((contact) => ( + <SelectItem + key={contact.id} + value={contact.id.toString()} + > + {contact.contactName} {contact.isPrimary ? "(Primary)" : ""} + </SelectItem> + )) + ) : ( + <SelectItem value="none" disabled> + No contacts available + </SelectItem> + )} + </SelectContent> + </Select> + <FormMessage /> + </FormItem> + )} + /> + + {/* Display selected contact info (read-only) */} + {selectedVendorContact && ( + <> + <FormItem className="pb-2"> + <FormLabel>Contact Email</FormLabel> + <div className="p-2 border rounded-md bg-muted"> + {selectedVendorContact.contactEmail} + </div> + </FormItem> + + <FormItem className="pb-2"> + <FormLabel>Contact Name</FormLabel> + <div className="p-2 border rounded-md bg-muted"> + {selectedVendorContact.contactName} + </div> + </FormItem> + + <FormItem className="pb-2"> + <FormLabel>Contact Position</FormLabel> + <div className="p-2 border rounded-md bg-muted"> + {selectedVendorContact.contactPosition || "N/A"} + </div> + </FormItem> + </> + )} + </CardContent> + </Card> + )} + </AccordionContent> + </AccordionItem> + </Accordion> + + <DialogFooter> + <Button type="button" variant="outline" onClick={() => onOpenChange(false)}> + Cancel + </Button> + <Button type="submit" disabled={isSubmitting}> + {isSubmitting ? "Sending..." : "Send Requests"} + </Button> + </DialogFooter> + </form> + </Form> + </DialogContent> + </Dialog> + ) +}
\ No newline at end of file |
