diff options
Diffstat (limited to 'components/vendor-data-plant')
| -rw-r--r-- | components/vendor-data-plant/project-swicher.tsx | 171 | ||||
| -rw-r--r-- | components/vendor-data-plant/sidebar.tsx | 318 | ||||
| -rw-r--r-- | components/vendor-data-plant/tag-table/add-tag-dialog.tsx | 357 | ||||
| -rw-r--r-- | components/vendor-data-plant/tag-table/tag-table-column.tsx | 198 | ||||
| -rw-r--r-- | components/vendor-data-plant/tag-table/tag-table.tsx | 39 | ||||
| -rw-r--r-- | components/vendor-data-plant/tag-table/tag-type-definitions.ts | 87 | ||||
| -rw-r--r-- | components/vendor-data-plant/vendor-data-container.tsx | 505 |
7 files changed, 1675 insertions, 0 deletions
diff --git a/components/vendor-data-plant/project-swicher.tsx b/components/vendor-data-plant/project-swicher.tsx new file mode 100644 index 00000000..d3123709 --- /dev/null +++ b/components/vendor-data-plant/project-swicher.tsx @@ -0,0 +1,171 @@ +"use client" + +import * as React from "react" +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" +import { Check, ChevronsUpDown, Loader2 } from "lucide-react" + +interface ContractInfo { + contractId: number + contractName: string +} + +interface ProjectInfo { + projectId: number + projectCode: string + projectName: string + contracts: ContractInfo[] +} + +interface ProjectSwitcherProps { + isCollapsed: boolean + projects: ProjectInfo[] + + // 상위가 관리하는 "현재 선택된 contractId" + selectedContractId: number | null + + // 콜백: 사용자가 "어떤 contract"를 골랐는지 + // => 우리가 projectId도 찾아서 상위 state를 같이 갱신해야 함 + onSelectContract: (projectId: number, contractId: number) => void + + // 로딩 상태 (선택사항) + isLoading?: boolean +} + +export function ProjectSwitcher({ + isCollapsed, + projects, + selectedContractId, + onSelectContract, + isLoading = false, +}: ProjectSwitcherProps) { + const [popoverOpen, setPopoverOpen] = React.useState(false) + const [searchTerm, setSearchTerm] = React.useState("") + + // 현재 선택된 contract 객체 찾기 + const selectedContract = React.useMemo(() => { + if (!selectedContractId) return null + for (const proj of projects) { + const found = proj.contracts.find((c) => c.contractId === selectedContractId) + if (found) { + return { ...found, projectId: proj.projectId, projectName: proj.projectName } + } + } + return null + }, [projects, selectedContractId]) + + // Trigger label => 계약 이름 or placeholder + const triggerLabel = selectedContract?.contractName ?? "Select a contract" + + // 검색어에 따른 필터링된 프로젝트/계약 목록 + const filteredProjects = React.useMemo(() => { + if (!searchTerm) return projects + + return projects.map(project => ({ + ...project, + contracts: project.contracts.filter(contract => + contract.contractName.toLowerCase().includes(searchTerm.toLowerCase()) || + project.projectName.toLowerCase().includes(searchTerm.toLowerCase()) + ) + })).filter(project => project.contracts.length > 0) + }, [projects, searchTerm]) + + // 계약 선택 핸들러 + function handleSelectContract(projectId: number, contractId: number) { + onSelectContract(projectId, contractId) + setPopoverOpen(false) + setSearchTerm("") // 검색어 초기화 + } + + // 총 계약 수 계산 (빈 상태 표시용) + const totalContracts = filteredProjects.reduce((sum, project) => sum + project.contracts.length, 0) + + return ( + <Popover open={popoverOpen} onOpenChange={setPopoverOpen}> + <PopoverTrigger asChild> + <Button + type="button" + variant="outline" + className={cn( + "justify-between relative", + isCollapsed ? "h-9 w-9 shrink-0 items-center justify-center p-0" : "w-full h-9" + )} + disabled={isLoading} + aria-label="Select Contract" + > + {isLoading ? ( + <> + <span className={cn(isCollapsed && "hidden")}>Loading...</span> + <Loader2 className={cn("h-4 w-4 animate-spin", !isCollapsed && "ml-2")} /> + </> + ) : ( + <> + <span className={cn("truncate flex-grow text-left", isCollapsed && "hidden")}> + {triggerLabel} + </span> + <ChevronsUpDown className={cn("h-4 w-4 opacity-50 flex-shrink-0", isCollapsed && "hidden")} /> + </> + )} + </Button> + </PopoverTrigger> + + <PopoverContent className="w-[320px] p-0" align="start"> + <Command> + <CommandInput + placeholder="Search contracts..." + value={searchTerm} + onValueChange={setSearchTerm} + /> + + <CommandList + className="max-h-[320px]" + onWheel={(e) => { + e.stopPropagation() // 이벤트 전파 차단 + const target = e.currentTarget + target.scrollTop += e.deltaY // 직접 스크롤 처리 + }} + > + <CommandEmpty> + {totalContracts === 0 ? "No contracts found." : "No search results."} + </CommandEmpty> + + {filteredProjects.map((project) => ( + <CommandGroup key={project.projectCode} heading={project.projectName}> + {project.contracts.map((contract) => ( + <CommandItem + key={contract.contractId} + onSelect={() => handleSelectContract(project.projectId, contract.contractId)} + value={`${project.projectName} ${contract.contractName}`} + className="truncate" + title={contract.contractName} + > + <span className="truncate">{contract.contractName}</span> + <Check + className={cn( + "ml-auto h-4 w-4 flex-shrink-0", + selectedContractId === contract.contractId ? "opacity-100" : "opacity-0" + )} + /> + </CommandItem> + ))} + </CommandGroup> + ))} + </CommandList> + </Command> + </PopoverContent> + </Popover> + ) +}
\ No newline at end of file diff --git a/components/vendor-data-plant/sidebar.tsx b/components/vendor-data-plant/sidebar.tsx new file mode 100644 index 00000000..31ee6dc7 --- /dev/null +++ b/components/vendor-data-plant/sidebar.tsx @@ -0,0 +1,318 @@ +"use client" + +import * as React from "react" +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { ScrollArea } from "@/components/ui/scroll-area" +import { Separator } from "@/components/ui/separator" +import { + Tooltip, + TooltipTrigger, + TooltipContent, +} from "@/components/ui/tooltip" +import { Package2, FormInput } from "lucide-react" +import { useRouter, usePathname } from "next/navigation" +import { Skeleton } from "@/components/ui/skeleton" +import { type FormInfo } from "@/lib/forms/services" + +interface PackageData { + itemId: number + itemName: string +} + +interface SidebarProps extends React.HTMLAttributes<HTMLDivElement> { + isCollapsed: boolean + packages: PackageData[] + selectedPackageId: number | null + selectedProjectId: number | null + selectedContractId: number | null + onSelectPackage: (itemId: number) => void + forms?: FormInfo[] + onSelectForm: (formName: string) => void + isLoadingForms?: boolean + mode: "IM" | "ENG" +} + +export function Sidebar({ + className, + isCollapsed, + packages, + selectedPackageId, + selectedProjectId, + selectedContractId, + onSelectPackage, + forms, + onSelectForm, + isLoadingForms = false, + mode = "IM", +}: SidebarProps) { + const router = useRouter() + const rawPathname = usePathname() + const pathname = rawPathname ?? "" + + /** + * --------------------------- + * 1) URL에서 현재 패키지 / 폼 코드 추출 + * --------------------------- + */ + const segments = pathname.split("/").filter(Boolean) + + let currentItemId: number | null = null + let currentFormCode: string | null = null + + const tagIndex = segments.indexOf("tag") + if (tagIndex !== -1 && segments[tagIndex + 1]) { + currentItemId = parseInt(segments[tagIndex + 1], 10) + } + + const formIndex = segments.indexOf("form") + if (formIndex !== -1) { + const itemSegment = segments[formIndex + 1] + const codeSegment = segments[formIndex + 2] + + if (itemSegment) { + currentItemId = parseInt(itemSegment, 10) + } + if (codeSegment) { + currentFormCode = codeSegment + } + } + + /** + * --------------------------- + * 2) 패키지 클릭 핸들러 (IM 모드) + * --------------------------- + */ + const handlePackageClick = (itemId: number) => { + // 상위 컴포넌트 상태 업데이트 + onSelectPackage(itemId) + + // 해당 태그 페이지로 라우팅 + // 예: /vendor-data-plant/tag/123 + const baseSegments = segments.slice(0, segments.indexOf("vendor-data-plant") + 1).join("/") + router.push(`/${baseSegments}/tag/${itemId}`) + } + + /** + * --------------------------- + * 3) 폼 클릭 핸들러 (IM 모드만 사용) + * --------------------------- + */ + const handleFormClick = (form: FormInfo) => { + // IM 모드에서만 사용 + if (selectedPackageId === null) return; + + onSelectForm(form.formName) + + const baseSegments = segments.slice(0, segments.indexOf("vendor-data-plant") + 1).join("/") + router.push(`/${baseSegments}/form/${selectedPackageId}/${form.formCode}/${selectedProjectId}/${selectedContractId}?mode=${mode}`) + } + + /** + * --------------------------- + * 4) 패키지 클릭 핸들러 (ENG 모드) + * --------------------------- + */ + const handlePackageUnderFormClick = (form: FormInfo, pkg: PackageData) => { + onSelectForm(form.formName) + onSelectPackage(pkg.itemId) + + const baseSegments = segments.slice(0, segments.indexOf("vendor-data-plant") + 1).join("/") + router.push(`/${baseSegments}/form/${pkg.itemId}/${form.formCode}/${selectedProjectId}/${selectedContractId}?mode=${mode}`) + } + + return ( + <div className={cn("pb-12", className)}> + <div className="space-y-4 py-4"> + {/* ---------- 패키지(Items) 목록 - IM 모드에서만 표시 ---------- */} + {mode === "IM" && ( + <> + <div className="py-1"> + <h2 className="relative px-7 text-lg font-semibold tracking-tight"> + {isCollapsed ? "P" : "Package Lists"} + </h2> + <ScrollArea className="h-[150px] px-1"> + <div className="space-y-1 p-2"> + {packages.map((pkg) => { + const isActive = pkg.itemId === currentItemId + + return ( + <div key={pkg.itemId}> + {isCollapsed ? ( + <Tooltip delayDuration={0}> + <TooltipTrigger asChild> + <Button + variant="ghost" + className={cn( + "w-full justify-start font-normal", + isActive && "bg-accent text-accent-foreground" + )} + onClick={() => handlePackageClick(pkg.itemId)} + > + <Package2 className="mr-2 h-4 w-4" /> + </Button> + </TooltipTrigger> + <TooltipContent side="right"> + {pkg.itemName} + </TooltipContent> + </Tooltip> + ) : ( + <Button + variant="ghost" + className={cn( + "w-full justify-start font-normal", + isActive && "bg-accent text-accent-foreground" + )} + onClick={() => handlePackageClick(pkg.itemId)} + > + <Package2 className="mr-2 h-4 w-4" /> + {pkg.itemName} + </Button> + )} + </div> + ) + })} + </div> + </ScrollArea> + </div> + <Separator /> + </> + )} + + {/* ---------- 폼 목록 (IM 모드) / 패키지와 폼 목록 (ENG 모드) ---------- */} + <div className="py-1"> + <h2 className="relative px-7 text-lg font-semibold tracking-tight"> + {isCollapsed + ? (mode === "IM" ? "F" : "P") + : (mode === "IM" ? "Form Lists" : "Package Lists") + } + </h2> + <ScrollArea className={cn( + "px-1", + mode === "IM" ? "h-[300px]" : "h-[450px]" + )}> + <div className="space-y-1 p-2"> + {isLoadingForms ? ( + Array.from({ length: 3 }).map((_, index) => ( + <div key={`form-skeleton-${index}`} className="px-2 py-1.5"> + <Skeleton className="h-8 w-full" /> + </div> + )) + ) : mode === "IM" ? ( + // =========== IM 모드: 폼만 표시 =========== + !forms || forms.length === 0 ? ( + <p className="text-sm text-muted-foreground px-2"> + (No forms loaded) + </p> + ) : ( + forms.map((form) => { + const isFormActive = form.formCode === currentFormCode + const isDisabled = currentItemId === null + + return isCollapsed ? ( + <Tooltip key={form.formCode} delayDuration={0}> + <TooltipTrigger asChild> + <Button + variant="ghost" + className={cn( + "w-full justify-start font-normal", + isFormActive && "bg-accent text-accent-foreground" + )} + onClick={() => handleFormClick(form)} + disabled={isDisabled} + > + <FormInput className="mr-2 h-4 w-4" /> + </Button> + </TooltipTrigger> + <TooltipContent side="right"> + {form.formName} + </TooltipContent> + </Tooltip> + ) : ( + <Button + key={form.formCode} + variant="ghost" + className={cn( + "w-full justify-start font-normal", + isFormActive && "bg-accent text-accent-foreground" + )} + onClick={() => handleFormClick(form)} + disabled={isDisabled} + > + <FormInput className="mr-2 h-4 w-4" /> + {form.formName} + </Button> + ) + }) + ) + ) : ( + // =========== ENG 모드: 패키지 > 폼 계층 구조 =========== + packages.length === 0 ? ( + <p className="text-sm text-muted-foreground px-2"> + (No packages loaded) + </p> + ) : ( + packages.map((pkg) => ( + <div key={pkg.itemId} className="space-y-1"> + {isCollapsed ? ( + <Tooltip delayDuration={0}> + <TooltipTrigger asChild> + <div className="px-2 py-1"> + <Package2 className="h-4 w-4" /> + </div> + </TooltipTrigger> + <TooltipContent side="right"> + {pkg.itemName} + </TooltipContent> + </Tooltip> + ) : ( + <> + {/* 패키지 이름 (클릭 불가능한 라벨) */} + <div className="flex items-center px-2 py-1 text-sm font-medium"> + <Package2 className="mr-2 h-4 w-4" /> + {pkg.itemName} + </div> + + {/* 폼 목록 바로 표시 */} + <div className="ml-6 space-y-1"> + {!forms || forms.length === 0 ? ( + <p className="text-xs text-muted-foreground px-2 py-1"> + No forms available + </p> + ) : ( + forms.map((form) => { + const isFormPackageActive = + pkg.itemId === currentItemId && + form.formCode === currentFormCode + + return ( + <Button + key={`${pkg.itemId}-${form.formCode}`} + variant="ghost" + size="sm" + className={cn( + "w-full justify-start font-normal text-sm", + isFormPackageActive && "bg-accent text-accent-foreground" + )} + onClick={() => handlePackageUnderFormClick(form, pkg)} + > + <FormInput className="mr-2 h-3 w-3" /> + {form.formName} + </Button> + ) + }) + )} + </div> + </> + )} + </div> + )) + ) + )} + </div> + </ScrollArea> + </div> + </div> + </div> + ) +}
\ No newline at end of file diff --git a/components/vendor-data-plant/tag-table/add-tag-dialog.tsx b/components/vendor-data-plant/tag-table/add-tag-dialog.tsx new file mode 100644 index 00000000..1321fc58 --- /dev/null +++ b/components/vendor-data-plant/tag-table/add-tag-dialog.tsx @@ -0,0 +1,357 @@ +"use client" + +import * as React from "react" +import { useForm, useWatch } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import { + Dialog, + DialogTrigger, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, +} from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { createTagSchema, type CreateTagSchema } from "@/lib/tags/validations" +import { createTag } from "@/lib/tags/service" +import { toast } from "sonner" +import { Loader2 } from "lucide-react" +import { cn } from "@/lib/utils" +import { useRouter } from "next/navigation" + +// Popover + Command +import { + Popover, + PopoverTrigger, + PopoverContent, +} from "@/components/ui/popover" +import { + Command, + CommandInput, + CommandList, + CommandGroup, + CommandItem, + CommandEmpty, +} from "@/components/ui/command" +import { ChevronsUpDown, Check } from "lucide-react" + +// The dynamic Tag Type definitions +import { tagTypeDefinitions } from "./tag-type-definitions" + +// Add Select component for dropdown fields +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { ScrollArea } from "@/components/ui/scroll-area" + +interface AddTagDialogProps { + selectedPackageId: number | null +} + +export function AddTagDialog({ selectedPackageId }: AddTagDialogProps) { + const [open, setOpen] = React.useState(false) + const [isPending, startTransition] = React.useTransition() + const router = useRouter() + + const form = useForm<CreateTagSchema>({ + resolver: zodResolver(createTagSchema), + defaultValues: { + tagType: "", // user picks + tagNo: "", // auto-generated + description: "", + functionCode: "", + seqNumber: "", + valveAcronym: "", + processUnit: "", + }, + }) + + const watchAll = useWatch({ control: form.control }) + + // 1) Find the selected tag type definition + const currentTagTypeDef = React.useMemo(() => { + return tagTypeDefinitions.find((def) => def.id === watchAll.tagType) || null + }, [watchAll.tagType]) + + // 2) Whenever the user changes sub-fields, re-generate `tagNo` + React.useEffect(() => { + if (!currentTagTypeDef) { + // if no type selected, no auto-generation + return + } + + // Prevent infinite loop by excluding tagNo from the watched dependencies + // This is crucial because setting tagNo would trigger another update + const { tagNo, ...fieldsToWatch } = watchAll + + const newTagNo = currentTagTypeDef.generateTagNo(fieldsToWatch as CreateTagSchema) + + // Only update if different to avoid unnecessary re-renders + if (form.getValues("tagNo") !== newTagNo) { + form.setValue("tagNo", newTagNo, { shouldValidate: false }) + } + }, [currentTagTypeDef, watchAll, form]) + + // Check if tag number is valid (doesn't contain '??' and is not empty) + const isTagNoValid = React.useMemo(() => { + const tagNo = form.getValues("tagNo"); + return tagNo && tagNo.trim() !== "" && !tagNo.includes("??"); + }, [form, watchAll.tagNo]); + + // onSubmit + async function onSubmit(data: CreateTagSchema) { + startTransition(async () => { + if (!selectedPackageId) { + toast.error("No selectedPackageId.") + return + } + + const result = await createTag(data, selectedPackageId) + if ("error" in result) { + toast.error(`Error: ${result.error}`) + return + } + + toast.success("Tag created successfully!") + form.reset() + setOpen(false) + router.refresh() + + }) + } + + function handleDialogOpenChange(nextOpen: boolean) { + if (!nextOpen) { + form.reset() + } + setOpen(nextOpen) + } + + // 3) TagType selection UI (like your Command menu) + function renderTagTypeSelector(field: any) { + const [popoverOpen, setPopoverOpen] = React.useState(false) + return ( + <FormItem> + <FormLabel>Tag Type</FormLabel> + <FormControl> + <Popover open={popoverOpen} onOpenChange={setPopoverOpen}> + <PopoverTrigger asChild> + <Button + variant="outline" + role="combobox" + aria-expanded={popoverOpen} + className="w-full justify-between" + > + {field.value + ? tagTypeDefinitions.find((d) => d.id === field.value)?.label + : "Select Tag Type..."} + <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> + </Button> + </PopoverTrigger> + <PopoverContent className="w-full p-0"> + <Command> + <CommandInput placeholder="Search Tag Type..." /> + <CommandList> + <CommandEmpty>No tag type found.</CommandEmpty> + <CommandGroup> + {tagTypeDefinitions.map((def,index) => ( + <CommandItem + key={index} + onSelect={() => { + field.onChange(def.id) // store the 'id' + setPopoverOpen(false) + }} + value={def.id} + > + {def.label} + <Check + className={cn( + "ml-auto h-4 w-4", + field.value === def.id + ? "opacity-100" + : "opacity-0" + )} + /> + </CommandItem> + ))} + </CommandGroup> + </CommandList> + </Command> + </PopoverContent> + </Popover> + </FormControl> + <FormMessage /> + </FormItem> + ) + } + + // 4) Render sub-fields based on currentTagTypeDef + // Updated to handle different field types (text, select) + function renderSubFields() { + if (!currentTagTypeDef) return null + + return currentTagTypeDef.subFields.map((subField, index) => ( + + <FormField + key={`${subField.name}-${index}`} + control={form.control} + name={subField.name as keyof CreateTagSchema} + render={({ field }) => ( + <FormItem> + <FormLabel>{subField.label}</FormLabel> + <FormControl> + {subField.type === "select" && subField.options ? ( + <Select + value={field.value || ""} + onValueChange={field.onChange} + > + <SelectTrigger className="w-full"> + <SelectValue placeholder={subField.placeholder || "Select an option"} /> + </SelectTrigger> + <SelectContent> + {subField.options.map((option, index) => ( + <SelectItem key={index} value={option.value}> + {option.label} + </SelectItem> + ))} + </SelectContent> + </Select> + ) : ( + <Input + placeholder={subField.placeholder || ""} + value={field.value || ""} + onChange={field.onChange} + onBlur={field.onBlur} + name={field.name} + ref={field.ref} + /> + )} + </FormControl> + {subField.formatHint && ( + <p className="text-sm text-muted-foreground mt-1"> + {subField.formatHint} + </p> + )} + <FormMessage /> + </FormItem> + )} + /> + )) + } + + return ( + <Dialog open={open} onOpenChange={handleDialogOpenChange}> + <DialogTrigger asChild> + <Button variant="default" size="sm"> + Add Tag + </Button> + </DialogTrigger> + + <DialogContent className="max-w-md max-h-[90vh] overflow-y-auto"> + <DialogHeader> + <DialogTitle>Add New Tag</DialogTitle> + <DialogDescription> + Select a Tag Type and fill in sub-fields. The Tag No will be generated automatically. + </DialogDescription> + </DialogHeader> + <ScrollArea className="flex-1"> + + <Form {...form}> + <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4"> + <div className="space-y-4"> + {/* Tag Type - Outside ScrollArea as it's always visible */} + <FormField + control={form.control} + name="tagType" + render={({ field }) => renderTagTypeSelector(field)} + /> + </div> + + {/* ScrollArea for dynamic fields */} + <ScrollArea className="h-[50vh] pr-4"> + <div className="space-y-4"> + {/* sub-fields from the selected tagType */} + {renderSubFields()} + + {/* Tag No (auto-generated) */} + <FormField + control={form.control} + name="tagNo" + render={({ field }) => ( + <FormItem> + <FormLabel>Tag No (auto-generated)</FormLabel> + <FormControl> + <Input + placeholder="Auto-generated..." + {...field} + readOnly + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + {/* Description (optional) */} + <FormField + control={form.control} + name="description" + render={({ field }) => ( + <FormItem> + <FormLabel>Description </FormLabel> + <FormControl> + <Input + placeholder="Optional desc..." + value={field.value ?? ""} + onChange={(e) => field.onChange(e.target.value)} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + </div> + </ScrollArea> + </form> + </Form> + </ScrollArea> + + <DialogFooter> + <Button + type="button" + variant="outline" + onClick={() => { + form.reset(); + setOpen(false); + }} + disabled={isPending} + > + Cancel + </Button> + <Button type="submit" disabled={isPending || !isTagNoValid}> + {isPending && ( + <Loader2 className="mr-2 h-4 w-4 animate-spin" /> + )} + Create + </Button> + </DialogFooter> + + + </DialogContent> + </Dialog> + ) +}
\ No newline at end of file diff --git a/components/vendor-data-plant/tag-table/tag-table-column.tsx b/components/vendor-data-plant/tag-table/tag-table-column.tsx new file mode 100644 index 00000000..6f0d977f --- /dev/null +++ b/components/vendor-data-plant/tag-table/tag-table-column.tsx @@ -0,0 +1,198 @@ +"use client" + +import * as React from "react" +import { ColumnDef } from "@tanstack/react-table" +import type { Row } from "@tanstack/react-table" +import { numericFilter } from "@/lib/data-table" +import { ClientDataTableColumnHeaderSimple } from "@/components/client-data-table/data-table-column-simple-header" +import { formatDate } from "@/lib/utils" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Checkbox } from "@/components/ui/checkbox" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuTrigger, + } from "@/components/ui/dropdown-menu" +import { Ellipsis } from "lucide-react" +import { Tag } from "@/types/vendorData" +import { createFilterFn } from "@/components/client-data-table/table-filters" + + +export interface DataTableRowAction<TData> { + row: Row<TData> + type: 'open' | "update" | "delete" +} + +interface GetColumnsProps { + setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<Tag> | null>> +} + +export function getColumns({ + setRowAction, + }: GetColumnsProps): ColumnDef<Tag>[] { + return [ + { + id: "select", + header: ({ table }) => ( + <Checkbox + checked={ + table.getIsAllPageRowsSelected() || + (table.getIsSomePageRowsSelected() && "indeterminate") + } + onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + className="translate-y-0.5" + /> + ), + cell: ({ row }) => ( + <Checkbox + checked={row.getIsSelected()} + onCheckedChange={(value) => row.toggleSelected(!!value)} + aria-label="Select row" + className="translate-y-0.5" + /> + ), + enableSorting: false, + enableHiding: false, + size: 40, + }, + + { + accessorKey: "tagNo", + header: ({ column }) => ( + <ClientDataTableColumnHeaderSimple column={column} title="Tag No." /> + ), + filterFn: createFilterFn("text"), + cell: ({ row }) => <div className="w-20">{row.getValue("tagNo")}</div>, + meta: { + excelHeader: "Tag No" + }, + }, + { + accessorKey: "description", + header: ({ column }) => ( + <ClientDataTableColumnHeaderSimple column={column} title="Tag Description" /> + ), + cell: ({ row }) => <div className="w-120">{row.getValue("description")}</div>, + meta: { + excelHeader: "Tag Descripiton" + }, + }, + { + accessorKey: "tagType", + header: ({ column }) => ( + <ClientDataTableColumnHeaderSimple column={column} title="Tag Type" /> + ), + cell: ({ row }) => <div className="w-40">{row.getValue("tagType")}</div>, + meta: { + excelHeader: "Tag Type" + }, + }, + { + id: "validation", + header: "Error", + cell: ({ row }) => <div className="w-100"></div>, + }, + + { + accessorKey: "createdAt", + header: ({ column }) => ( + <ClientDataTableColumnHeaderSimple column={column} title="Created At" /> + ), + cell: ({ cell }) => formatDate(cell.getValue() as Date), + meta: { + excelHeader: "created At" + }, + }, + { + accessorKey: "updatedAt", + header: ({ column }) => ( + <ClientDataTableColumnHeaderSimple column={column} title="Updated At" /> + ), + cell: ({ cell }) => formatDate(cell.getValue() as Date), + meta: { + excelHeader: "updated At" + }, + }, + + { + id: "actions", + cell: function Cell({ row }) { + const [isUpdatePending, startUpdateTransition] = React.useTransition() + + return ( + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button + aria-label="Open menu" + variant="ghost" + className="flex size-8 p-0 data-[state=open]:bg-muted" + > + <Ellipsis className="size-4" aria-hidden="true" /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end" className="w-40"> + <DropdownMenuItem + onSelect={() => setRowAction({ row, type: "update" })} + > + Edit + </DropdownMenuItem> + <DropdownMenuSub> + <DropdownMenuSubTrigger>Labels</DropdownMenuSubTrigger> + <DropdownMenuSubContent> + {/* <DropdownMenuRadioGroup + value={row.original.label} + onValueChange={(value) => { + startUpdateTransition(() => { + toast.promise( + updateTask({ + id: row.original.id, + label: value as Task["label"], + }), + { + loading: "Updating...", + success: "Label updated", + error: (err) => getErrorMessage(err), + } + ) + }) + }} + > + {tasks.label.enumValues.map((label) => ( + <DropdownMenuRadioItem + key={label} + value={label} + className="capitalize" + disabled={isUpdatePending} + > + {label} + </DropdownMenuRadioItem> + ))} + </DropdownMenuRadioGroup> */} + </DropdownMenuSubContent> + </DropdownMenuSub> + <DropdownMenuSeparator /> + <DropdownMenuItem + onSelect={() => setRowAction({ row, type: "delete" })} + > + Delete + <DropdownMenuShortcut>⌘⌫</DropdownMenuShortcut> + </DropdownMenuItem> + </DropdownMenuContent> + </DropdownMenu> + ) + }, + size: 40, + }, + ] + } +
\ No newline at end of file diff --git a/components/vendor-data-plant/tag-table/tag-table.tsx b/components/vendor-data-plant/tag-table/tag-table.tsx new file mode 100644 index 00000000..a449529f --- /dev/null +++ b/components/vendor-data-plant/tag-table/tag-table.tsx @@ -0,0 +1,39 @@ +"use client" + +import * as React from "react" +import { ClientDataTable } from "@/components/client-data-table/data-table" +import { DataTableRowAction, getColumns } from "./tag-table-column" +import { Tag as TagData } from "@/types/vendorData" +import { DataTableAdvancedFilterField } from "@/types/table" +import { AddTagDialog } from "./add-tag-dialog" + +interface TagTableProps { + data: TagData[] +} + +/** + * TagTable: Tag 데이터를 표시하는 표 + */ +export function TagTable({ data }: TagTableProps) { + + const [rowAction, setRowAction] = + React.useState<DataTableRowAction<TagData> | null>(null) + + const columns = React.useMemo( + () => getColumns({ setRowAction }), + [setRowAction] + ) + + const advancedFilterFields: DataTableAdvancedFilterField<TagData>[] = [] + + return ( + <> + <ClientDataTable + data={data} + columns={columns} + advancedFilterFields={advancedFilterFields} + /> + + </> + ) +}
\ No newline at end of file diff --git a/components/vendor-data-plant/tag-table/tag-type-definitions.ts b/components/vendor-data-plant/tag-table/tag-type-definitions.ts new file mode 100644 index 00000000..e5d04eab --- /dev/null +++ b/components/vendor-data-plant/tag-table/tag-type-definitions.ts @@ -0,0 +1,87 @@ +import { CreateTagSchema } from "@/lib/tags/validations" + +/** + * Each "Tag Type" has: + * - id, label + * - subFields[]: + * -- name (form field name) + * -- label (UI label) + * -- placeholder? + * -- type: "select" | "text" + * -- options?: { value: string; label: string; }[] (for dropdown) + * -- optional "regex" or "formatHint" for display + * - generateTagNo: function + */ +export const tagTypeDefinitions = [ + { + id: "EquipmentNumbering", + label: "Equipment Numbering", + subFields: [ + { + name: "functionCode", + label: "Function", + placeholder: "", + type: "select", + // Example options: + options: [ + { value: "PM", label: "Pump" }, + { value: "AA", label: "Pneumatic Motor" }, + ], + // or if you want a regex or format hint: + formatHint: "2 letters, e.g. PM", + }, + { + name: "seqNumber", + label: "Seq. Number", + placeholder: "001", + type: "text", + formatHint: "3 digits", + }, + ], + generateTagNo: (values: CreateTagSchema) => { + const fc = values.functionCode || "??" + const seq = values.seqNumber || "000" + return `${fc}-${seq}` + }, + }, + { + id: "Valve", + label: "Valve", + subFields: [ + { + name: "valveAcronym", + label: "Valve Acronym", + placeholder: "", + type: "select", + options: [ + { value: "VB", label: "Ball Valve" }, + { value: "VAR", label: "Auto Recirculation Valve" }, + ], + }, + { + name: "processUnit", + label: "Process Unit (2 digits)", + placeholder: "01", + type: "select", + options: [ + { value: "01", label: "Firewater System" }, + { value: "02", label: "Liquefaction Unit" }, + ], + }, + { + name: "seqNumber", + label: "Seq. Number", + placeholder: "001", + type: "text", + formatHint: "3 digits", + }, + ], + generateTagNo: (values: CreateTagSchema) => { + const va = values.valveAcronym || "??" + const pu = values.processUnit || "??" + const seq= values.seqNumber || "000" + return `${va}-${pu}${seq}` + }, + }, + // ... more types from your API ... +]
\ No newline at end of file diff --git a/components/vendor-data-plant/vendor-data-container.tsx b/components/vendor-data-plant/vendor-data-container.tsx new file mode 100644 index 00000000..60ec2c94 --- /dev/null +++ b/components/vendor-data-plant/vendor-data-container.tsx @@ -0,0 +1,505 @@ +"use client" + +import * as React from "react" +import { TooltipProvider } from "@/components/ui/tooltip" +import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable" +import { cn } from "@/lib/utils" +import { ProjectSwitcher } from "./project-swicher" +import { Sidebar } from "./sidebar" +import { usePathname, useRouter, useSearchParams } from "next/navigation" +import { getFormsByContractItemId, type FormInfo } from "@/lib/forms/services" +import { Separator } from "@/components/ui/separator" +import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs" +import { ScrollArea } from "@/components/ui/scroll-area" +import { Button } from "@/components/ui/button" +import { FormInput } from "lucide-react" +import { Skeleton } from "@/components/ui/skeleton" +import { selectedModeAtom } from '@/atoms' +import { useAtom } from 'jotai' + +interface PackageData { + itemId: number + itemName: string +} + +interface ContractData { + contractId: number + contractName: string + packages: PackageData[] +} + +interface ProjectData { + projectId: number + projectCode: string + projectName: string + projectType: string + contracts: ContractData[] +} + +interface VendorDataContainerProps { + projects: ProjectData[] + defaultLayout?: number[] + defaultCollapsed?: boolean + navCollapsedSize: number + children: React.ReactNode +} + +function getTagIdFromPathname(path: string | null): number | null { + if (!path) return null; + + // 태그 패턴 검사 (/tag/123) + const tagMatch = path.match(/\/tag\/(\d+)/) + if (tagMatch) return parseInt(tagMatch[1], 10) + + // 폼 패턴 검사 (/form/123/...) + const formMatch = path.match(/\/form\/(\d+)/) + if (formMatch) return parseInt(formMatch[1], 10) + + return null +} + +export function VendorDataContainer({ + projects, + defaultLayout = [20, 80], + defaultCollapsed = false, + navCollapsedSize, + children +}: VendorDataContainerProps) { + const pathname = usePathname() + const router = useRouter() + const searchParams = useSearchParams() + + const tagIdNumber = getTagIdFromPathname(pathname) + + // 기본 상태 + const [selectedProjectId, setSelectedProjectId] = React.useState(projects[0]?.projectId || 0) + const [isCollapsed, setIsCollapsed] = React.useState(defaultCollapsed) + const [selectedContractId, setSelectedContractId] = React.useState( + projects[0]?.contracts[0]?.contractId || 0 + ) + const [selectedPackageId, setSelectedPackageId] = React.useState<number | null>(null) + const [formList, setFormList] = React.useState<FormInfo[]>([]) + const [selectedFormCode, setSelectedFormCode] = React.useState<string | null>(null) + const [isLoadingForms, setIsLoadingForms] = React.useState(false) + + console.log(selectedPackageId,"selectedPackageId") + + + // 현재 선택된 프로젝트/계약/패키지 + const currentProject = projects.find((p) => p.projectId === selectedProjectId) ?? projects[0] + const currentContract = currentProject?.contracts.find((c) => c.contractId === selectedContractId) + ?? currentProject?.contracts[0] + + // 프로젝트 타입 확인 - ship인 경우 항상 ENG 모드 + const isShipProject = currentProject?.projectType === "ship" + + const [selectedMode, setSelectedMode] = useAtom(selectedModeAtom) + + // URL에서 모드 추출 (ship 프로젝트면 무조건 ENG로, 아니면 URL 또는 기본값) + const modeFromUrl = searchParams?.get('mode') + const initialMode ="ENG" + + // 모드 초기화 (기존의 useState 초기값 대신) + React.useEffect(() => { + setSelectedMode(initialMode as "IM" | "ENG") + }, [initialMode, setSelectedMode]) + + const isTagOrFormRoute = pathname ? (pathname.includes("/tag/") || pathname.includes("/form/")) : false + const currentPackageName = isTagOrFormRoute + ? currentContract?.packages.find((pkg) => pkg.itemId === selectedPackageId)?.itemName || "None" + : "None" + + // 폼 목록에서 고유한 폼 이름만 추출 + const formNames = React.useMemo(() => { + return [...new Set(formList.map((form) => form.formName))] + }, [formList]) + + // URL에서 현재 폼 코드 추출 + const getCurrentFormCode = (path: string): string | null => { + const segments = path.split("/").filter(Boolean) + const formIndex = segments.indexOf("form") + if (formIndex !== -1 && segments[formIndex + 2]) { + return segments[formIndex + 2] + } + return null + } + + const currentFormCode = React.useMemo(() => { + return pathname ? getCurrentFormCode(pathname) : null + }, [pathname]) + + // URL에서 모드가 변경되면 상태도 업데이트 (ship 프로젝트가 아닐 때만) + React.useEffect(() => { + if (!isShipProject) { + const modeFromUrl = searchParams?.get('mode') + if (modeFromUrl === "ENG" || modeFromUrl === "IM") { + setSelectedMode(modeFromUrl) + } + } + }, [searchParams, isShipProject]) + + // 프로젝트 타입이 변경될 때 모드 업데이트 + React.useEffect(() => { + if (isShipProject) { + setSelectedMode("ENG") + + // URL 모드 파라미터도 업데이트 + const url = new URL(window.location.href); + url.searchParams.set('mode', 'ENG'); + router.replace(url.pathname + url.search); + } + }, [isShipProject, router]) + + // (1) 새로고침 시 URL 파라미터(tagIdNumber) → selectedPackageId 세팅 + React.useEffect(() => { + if (!currentContract) return + + if (tagIdNumber) { + setSelectedPackageId(tagIdNumber) + } else { + // tagIdNumber가 없으면, 현재 계약의 첫 번째 패키지로 + if (currentContract.packages?.length) { + setSelectedPackageId(currentContract.packages[0].itemId) + } else { + setSelectedPackageId(null) + } + } + }, [tagIdNumber, currentContract]) + + // (2) 프로젝트 변경 시 계약 초기화 + // React.useEffect(() => { + // if (currentProject?.contracts.length) { + // setSelectedContractId(currentProject.contracts[0].contractId) + // } else { + // setSelectedContractId(0) + // } + // }, [currentProject]) + + // (3) 패키지 ID와 모드가 변경될 때마다 폼 로딩 + React.useEffect(() => { + const packageId = getTagIdFromPathname(pathname) + + if (packageId) { + setSelectedPackageId(packageId) + + // URL에서 패키지 ID를 얻었을 때 즉시 폼 로드 + loadFormsList(packageId, selectedMode); + } else if (currentContract?.packages?.length) { + const firstPackageId = currentContract.packages[0].itemId; + setSelectedPackageId(firstPackageId); + loadFormsList(firstPackageId, selectedMode); + } + }, [pathname, currentContract, selectedMode]) + + // 모드에 따른 폼 로드 함수 + const loadFormsList = async (packageId: number, mode: "IM" | "ENG") => { + if (!packageId) return; + + setIsLoadingForms(true); + try { + const result = await getFormsByContractItemId(packageId, mode); + setFormList(result.forms || []); + } catch (error) { + console.error(`폼 로딩 오류 (${mode} 모드):`, error); + setFormList([]); + } finally { + setIsLoadingForms(false); + } + }; + + // 핸들러들 +// 수정된 handleSelectContract 함수 +async function handleSelectContract(projId: number, cId: number) { + setSelectedProjectId(projId) + setSelectedContractId(cId) + + // 선택된 계약의 첫 번째 패키지 찾기 + const selectedProject = projects.find(p => p.projectId === projId) + const selectedContract = selectedProject?.contracts.find(c => c.contractId === cId) + + if (selectedContract?.packages?.length) { + const firstPackageId = selectedContract.packages[0].itemId + setSelectedPackageId(firstPackageId) + + // ENG 모드로 폼 목록 로드 + setIsLoadingForms(true) + try { + const result = await getFormsByContractItemId(firstPackageId, "ENG") + setFormList(result.forms || []) + + // 첫 번째 폼이 있으면 자동 선택 및 네비게이션 + if (result.forms && result.forms.length > 0) { + const firstForm = result.forms[0] + setSelectedFormCode(firstForm.formCode) + + // ENG 모드로 설정 + setSelectedMode("ENG") + + // 첫 번째 폼으로 네비게이션 + const baseSegments = pathname?.split("/").filter(Boolean).slice(0, pathname.split("/").filter(Boolean).indexOf("vendor-data-plant") + 1).join("/") + router.push(`/${baseSegments}/form/0/${firstForm.formCode}/${projId}/${cId}?mode=ENG`) + } else { + // 폼이 없는 경우에도 ENG 모드로 설정 + setSelectedMode("ENG") + setSelectedFormCode(null) + + const baseSegments = pathname?.split("/").filter(Boolean).slice(0, pathname.split("/").filter(Boolean).indexOf("vendor-data-plant") + 1).join("/") + router.push(`/${baseSegments}/form/0/0/${projId}/${cId}?mode=ENG`) + } + } catch (error) { + console.error("폼 로딩 오류:", error) + setFormList([]) + setSelectedFormCode(null) + + // 오류 발생 시에도 ENG 모드로 설정 + setSelectedMode("ENG") + } finally { + setIsLoadingForms(false) + } + } else { + // 패키지가 없는 경우 + setSelectedPackageId(null) + setFormList([]) + setSelectedFormCode(null) + setSelectedMode("ENG") + } +} + + function handleSelectPackage(itemId: number) { + setSelectedPackageId(itemId) + } + + function handleSelectForm(formName: string) { + const form = formList.find((f) => f.formName === formName) + if (form) { + setSelectedFormCode(form.formCode) + } + } + + // 모드 변경 핸들러 +// 모드 변경 핸들러 +const handleModeChange = async (mode: "IM" | "ENG") => { + // ship 프로젝트인 경우 모드 변경 금지 + if (isShipProject && mode !== "ENG") return; + + setSelectedMode(mode); + + // 모드가 변경될 때 자동 네비게이션 + if (currentContract?.packages?.length) { + const firstPackageId = currentContract.packages[0].itemId; + + if (mode === "IM") { + // IM 모드: 첫 번째 패키지로 이동 + const baseSegments = pathname?.split("/").filter(Boolean).slice(0, pathname.split("/").filter(Boolean).indexOf("vendor-data-plant") + 1).join("/"); + router.push(`/${baseSegments}/tag/${firstPackageId}?mode=${mode}`); + } else { + // ENG 모드: 폼 목록을 먼저 로드 + setIsLoadingForms(true); + try { + const result = await getFormsByContractItemId(firstPackageId, mode); + setFormList(result.forms || []); + + // 폼이 있으면 첫 번째 폼으로 이동 + if (result.forms && result.forms.length > 0) { + const firstForm = result.forms[0]; + setSelectedFormCode(firstForm.formCode); + + const baseSegments = pathname?.split("/").filter(Boolean).slice(0, pathname.split("/").filter(Boolean).indexOf("vendor-data-plant") + 1).join("/"); + router.push(`/${baseSegments}/form/0/${firstForm.formCode}/${selectedProjectId}/${selectedContractId}?mode=${mode}`); + } else { + // 폼이 없으면 모드만 변경 + const baseSegments = pathname?.split("/").filter(Boolean).slice(0, pathname.split("/").filter(Boolean).indexOf("vendor-data-plant") + 1).join("/"); + router.push(`/${baseSegments}/form/0/0/${selectedProjectId}/${selectedContractId}?mode=${mode}`); + } + } catch (error) { + console.error(`폼 로딩 오류 (${mode} 모드):`, error); + // 오류 발생 시 모드만 변경 + const url = new URL(window.location.href); + url.searchParams.set('mode', mode); + router.replace(url.pathname + url.search); + } finally { + setIsLoadingForms(false); + } + } + } else { + // 패키지가 없는 경우, 모드만 변경 + const url = new URL(window.location.href); + url.searchParams.set('mode', mode); + router.replace(url.pathname + url.search); + } +}; + + return ( + <TooltipProvider delayDuration={0}> + <ResizablePanelGroup direction="horizontal" className="h-full"> + <ResizablePanel + defaultSize={defaultLayout[0]} + collapsedSize={navCollapsedSize} + collapsible + minSize={15} + maxSize={25} + onCollapse={() => setIsCollapsed(true)} + onResize={() => setIsCollapsed(false)} + className={cn(isCollapsed && "min-w-[50px] transition-all duration-300 ease-in-out")} + > + <div + className={cn( + "flex h-[52px] items-center justify-center gap-2", + isCollapsed ? "h-[52px]" : "px-2" + )} + > + <ProjectSwitcher + isCollapsed={isCollapsed} + projects={projects} + selectedContractId={selectedContractId} + onSelectContract={handleSelectContract} + /> + </div> + <Separator /> + + {!isCollapsed ? ( + isShipProject ? ( + // 프로젝트 타입이 ship인 경우: 탭 없이 ENG 모드 사이드바만 바로 표시 + <div className="mt-0"> + <Sidebar + isCollapsed={isCollapsed} + packages={currentContract?.packages || []} + selectedPackageId={selectedPackageId} + selectedProjectId={selectedProjectId} + selectedContractId={selectedContractId} + onSelectPackage={handleSelectPackage} + forms={formList} + selectedForm={ + selectedFormCode + ? formList.find((f) => f.formCode === selectedFormCode)?.formName || null + : null + } + onSelectForm={handleSelectForm} + isLoadingForms={isLoadingForms} + mode="ENG" + className="hidden lg:block" + /> + </div> + ) : ( + // 프로젝트 타입이 ship이 아닌 경우: 기존 탭 UI 표시 + <Tabs + defaultValue={initialMode} + value={selectedMode} + onValueChange={(value) => handleModeChange(value as "IM" | "ENG")} + className="w-full" + > + <TabsList className="w-full"> + <TabsTrigger value="ENG" className="flex-1">Engineering</TabsTrigger> + <TabsTrigger value="IM" className="flex-1">Handover</TabsTrigger> + + </TabsList> + + <TabsContent value="IM" className="mt-0"> + <Sidebar + isCollapsed={isCollapsed} + packages={currentContract?.packages || []} + selectedPackageId={selectedPackageId} + selectedContractId={selectedContractId} + selectedProjectId={selectedProjectId} + onSelectPackage={handleSelectPackage} + forms={formList} + selectedForm={ + selectedFormCode + ? formList.find((f) => f.formCode === selectedFormCode)?.formName || null + : null + } + onSelectForm={handleSelectForm} + isLoadingForms={isLoadingForms} + mode="IM" + className="hidden lg:block" + /> + </TabsContent> + + <TabsContent value="ENG" className="mt-0"> + <Sidebar + isCollapsed={isCollapsed} + packages={currentContract?.packages || []} + selectedPackageId={selectedPackageId} + selectedContractId={selectedContractId} + selectedProjectId={selectedProjectId} + onSelectPackage={handleSelectPackage} + forms={formList} + selectedForm={ + selectedFormCode + ? formList.find((f) => f.formCode === selectedFormCode)?.formName || null + : null + } + onSelectForm={handleSelectForm} + isLoadingForms={isLoadingForms} + mode="ENG" + className="hidden lg:block" + /> + </TabsContent> + </Tabs> + ) + ) : ( + // 접혀있을 때 UI + <> + {!isShipProject && ( + // ship 프로젝트가 아닐 때만 모드 선택 버튼 표시 + <div className="flex justify-center space-x-1 my-2"> + + <Button + variant={selectedMode === "ENG" ? "default" : "ghost"} + size="sm" + className="h-8 px-2" + onClick={() => handleModeChange("ENG")} + > + Engineering + </Button> + <Button + variant={selectedMode === "IM" ? "default" : "ghost"} + size="sm" + className="h-8 px-2" + onClick={() => handleModeChange("IM")} + > + Handover + </Button> + </div> + )} + + <Sidebar + isCollapsed={isCollapsed} + packages={currentContract?.packages || []} + selectedPackageId={selectedPackageId} + selectedProjectId={selectedProjectId} + selectedContractId={selectedContractId} + onSelectPackage={handleSelectPackage} + forms={formList} + selectedForm={ + selectedFormCode + ? formList.find((f) => f.formCode === selectedFormCode)?.formName || null + : null + } + onSelectForm={handleSelectForm} + isLoadingForms={isLoadingForms} + mode={isShipProject ? "ENG" : selectedMode} + className="hidden lg:block" + /> + </> + )} + </ResizablePanel> + + <ResizableHandle withHandle /> + + <ResizablePanel defaultSize={defaultLayout[1]} minSize={40}> + <div className="p-4 h-full overflow-auto flex flex-col"> + <div className="flex items-center justify-between mb-4"> + <h2 className="text-lg font-bold"> + {isShipProject || selectedMode === "ENG" + ? "Engineering Mode" + : `Package: ${currentPackageName}`} + </h2> + </div> + {children} + </div> + </ResizablePanel> + </ResizablePanelGroup> + </TooltipProvider> + ) +}
\ No newline at end of file |
