diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-10-23 03:30:01 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-10-23 03:30:01 +0000 |
| commit | c8cccaf1198ae48754ac036b579732018f5b448a (patch) | |
| tree | 9c64024818c2be1c7b6699b4e141729432719d86 /lib/techsales-rfq | |
| parent | 835010104c25c370c1def1f2de52f518058f8b46 (diff) | |
(최겸) 기술영업 조선 rfq 수정(벤더, 담당자 임시삭제기능 추가)
Diffstat (limited to 'lib/techsales-rfq')
9 files changed, 454 insertions, 161 deletions
diff --git a/lib/techsales-rfq/service.ts b/lib/techsales-rfq/service.ts index 058ef48b..9a198ee5 100644 --- a/lib/techsales-rfq/service.ts +++ b/lib/techsales-rfq/service.ts @@ -2,8 +2,8 @@ import { unstable_noStore, revalidateTag, revalidatePath } from "next/cache";
import db from "@/db/db";
-import {
- techSalesRfqs,
+import {
+ techSalesRfqs,
techSalesVendorQuotations,
techSalesVendorQuotationRevisions,
techSalesAttachments,
@@ -13,7 +13,8 @@ import { users,
techSalesRfqComments,
techSalesRfqItems,
- biddingProjects
+ biddingProjects,
+ projectSeries
} from "@/db/schema";
import { and, desc, eq, ilike, or, sql, inArray, count, asc, lt, ne } from "drizzle-orm";
import { unstable_cache } from "@/lib/unstable-cache";
@@ -4209,6 +4210,7 @@ export async function getQuotationContacts(quotationId: number) { contactId: techSalesVendorQuotationContacts.contactId,
contactName: techVendorContacts.contactName,
contactPosition: techVendorContacts.contactPosition,
+ contactTitle: techVendorContacts.contactTitle,
contactEmail: techVendorContacts.contactEmail,
contactPhone: techVendorContacts.contactPhone,
contactCountry: techVendorContacts.contactCountry,
@@ -4320,15 +4322,23 @@ export async function getTechSalesRfqById(id: number) { pjtType: biddingProjects.pjtType,
ptypeNm: biddingProjects.ptypeNm,
projMsrm: biddingProjects.projMsrm,
+ pspid: biddingProjects.pspid,
})
.from(biddingProjects)
.where(eq(biddingProjects.id, rfq?.biddingProjectId ?? 0));
-
+
+ // 시리즈 정보 가져오기
+ const series = await db
+ .select()
+ .from(projectSeries)
+ .where(eq(projectSeries.pspid, project?.pspid ?? ""))
+ .orderBy(projectSeries.sersNo);
+
if (!rfq) {
return { data: null, error: "RFQ를 찾을 수 없습니다." };
}
-
- return { data: { ...rfq, project }, error: null };
+
+ return { data: { ...rfq, project, series }, error: null };
} catch (err) {
console.error("Error fetching RFQ:", err);
return { data: null, error: getErrorMessage(err) };
@@ -4339,6 +4349,7 @@ export async function getTechSalesRfqById(id: number) { export async function updateTechSalesRfq(data: {
id: number;
description: string;
+ remark: string;
dueDate: Date;
updatedBy: number;
}) {
@@ -4347,15 +4358,16 @@ export async function updateTechSalesRfq(data: { const rfq = await tx.query.techSalesRfqs.findFirst({
where: eq(techSalesRfqs.id, data.id),
});
-
+
if (!rfq) {
return { data: null, error: "RFQ를 찾을 수 없습니다." };
}
-
+
const [updatedRfq] = await tx
.update(techSalesRfqs)
.set({
description: data.description, // description 필드로 업데이트
+ remark: data.remark, // remark 필드로 업데이트
dueDate: data.dueDate,
updatedAt: new Date(),
})
diff --git a/lib/techsales-rfq/table/create-rfq-ship-dialog.tsx b/lib/techsales-rfq/table/create-rfq-ship-dialog.tsx index b851f7e8..035cd97e 100644 --- a/lib/techsales-rfq/table/create-rfq-ship-dialog.tsx +++ b/lib/techsales-rfq/table/create-rfq-ship-dialog.tsx @@ -4,6 +4,7 @@ import * as React from "react" import { toast } from "sonner"
import { ArrowUpDown, CheckSquare, Plus, Search, Square, X, Loader2 } from "lucide-react"
import { Input } from "@/components/ui/input"
+import { Textarea } from "@/components/ui/textarea"
import { Calendar } from "@/components/ui/calendar"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import { CalendarIcon } from "lucide-react"
@@ -228,9 +229,10 @@ export function CreateShipRfqDialog({ onCreated }: CreateShipRfqDialogProps) { return filtered
}, [allItems, itemSearchQuery, selectedWorkType, selectedShipType])
- // 사용 가능한 선종 목록 가져오기 (OPTION 제외)
+ // 사용 가능한 선종 목록 가져오기 (OPTION 제외, others는 맨 밑으로)
const availableShipTypes = React.useMemo(() => {
- return shipTypes.filter(shipType => shipType !== "OPTION")
+ const filtered = shipTypes.filter(shipType => shipType !== "OPTION")
+ return [...filtered.filter(type => type !== "OTHERS"), ...filtered.filter(type => type === "OTHERS")]
}, [shipTypes])
// 프로젝트 선택 처리
@@ -408,8 +410,9 @@ export function CreateShipRfqDialog({ onCreated }: CreateShipRfqDialogProps) { <FormItem>
<FormLabel>RFQ Context</FormLabel>
<FormControl>
- <Input
+ <Textarea
placeholder="RFQ Context를 입력하세요 (선택사항)"
+ className="min-h-[100px]"
{...field}
/>
</FormControl>
diff --git a/lib/techsales-rfq/table/detail-table/add-vendor-dialog.tsx b/lib/techsales-rfq/table/detail-table/add-vendor-dialog.tsx index ea982407..438ee840 100644 --- a/lib/techsales-rfq/table/detail-table/add-vendor-dialog.tsx +++ b/lib/techsales-rfq/table/detail-table/add-vendor-dialog.tsx @@ -362,99 +362,128 @@ export function AddVendorDialog({ )
// 벤더 구분자 설정 UI
- const renderVendorFlagsStep = () => (
- <div className="space-y-4">
- <div className="text-sm text-muted-foreground">
- 선택된 벤더들의 구분자를 설정해주세요. 각 벤더별로 여러 구분자를 선택할 수 있습니다.
- </div>
-
- {selectedVendorData.length > 0 ? (
- <div className="space-y-4">
- {selectedVendorData.map((vendor) => (
- <Card key={vendor.id}>
- <CardHeader className="pb-3">
- <CardTitle className="text-base">{vendor.vendorName}</CardTitle>
- <CardDescription>
- {vendor.vendorCode || 'N/A'} {vendor.country && `• ${vendor.country}`}
- </CardDescription>
- </CardHeader>
- <CardContent className="space-y-3">
- <div className="grid grid-cols-2 gap-3">
- <div className="flex items-center space-x-2">
- <Checkbox
- id={`customer-preferred-${vendor.id}`}
- checked={vendorFlags[vendor.id.toString()]?.isCustomerPreferred || false}
- onCheckedChange={(checked) =>
- handleVendorFlagChange(vendor.id, 'isCustomerPreferred', checked as boolean)
- }
- />
- <label
- htmlFor={`customer-preferred-${vendor.id}`}
- className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
- >
- 고객(선주) 선호 벤더
- </label>
- </div>
-
- <div className="flex items-center space-x-2">
- <Checkbox
- id={`new-discovery-${vendor.id}`}
- checked={vendorFlags[vendor.id.toString()]?.isNewDiscovery || false}
- onCheckedChange={(checked) =>
- handleVendorFlagChange(vendor.id, 'isNewDiscovery', checked as boolean)
- }
- />
- <label
- htmlFor={`new-discovery-${vendor.id}`}
- className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
- >
- 신규 발굴 벤더
- </label>
- </div>
-
- <div className="flex items-center space-x-2">
- <Checkbox
- id={`project-approved-${vendor.id}`}
- checked={vendorFlags[vendor.id.toString()]?.isProjectApproved || false}
- onCheckedChange={(checked) =>
- handleVendorFlagChange(vendor.id, 'isProjectApproved', checked as boolean)
- }
- />
- <label
- htmlFor={`project-approved-${vendor.id}`}
- className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
- >
- Project Approved Vendor
- </label>
- </div>
-
- <div className="flex items-center space-x-2">
- <Checkbox
- id={`shi-proposal-${vendor.id}`}
- checked={vendorFlags[vendor.id.toString()]?.isShiProposal || false}
- onCheckedChange={(checked) =>
- handleVendorFlagChange(vendor.id, 'isShiProposal', checked as boolean)
- }
- />
- <label
- htmlFor={`shi-proposal-${vendor.id}`}
- className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
- >
- SHI Proposal Vendor
- </label>
- </div>
- </div>
- </CardContent>
- </Card>
- ))}
- </div>
- ) : (
- <div className="text-center py-8 text-muted-foreground border rounded-md">
- 선택된 벤더가 없습니다
+ const renderVendorFlagsStep = () => {
+ const isShipRfq = selectedRfq?.rfqType === 'SHIP'
+
+ return (
+ <div className="space-y-4">
+ <div className="text-sm text-muted-foreground">
+ 선택된 벤더들의 구분자를 설정해주세요. 각 벤더별로 여러 구분자를 선택할 수 있습니다.
</div>
- )}
- </div>
- )
+
+ {selectedVendorData.length > 0 ? (
+ <div className="space-y-4">
+ {selectedVendorData.map((vendor) => (
+ <Card key={vendor.id}>
+ <CardHeader className="pb-3">
+ <CardTitle className="text-base">{vendor.vendorName}</CardTitle>
+ <CardDescription>
+ {vendor.vendorCode || 'N/A'} {vendor.country && `• ${vendor.country}`}
+ </CardDescription>
+ </CardHeader>
+ <CardContent className="space-y-3">
+ <div className="grid grid-cols-2 gap-3">
+ {/* 조선 RFQ인 경우: 고객 선호벤더, 신규발굴벤더, SHI Proposal Vendor 표시 */}
+ {isShipRfq && (
+ <>
+ <div className="flex items-center space-x-2">
+ <Checkbox
+ id={`customer-preferred-${vendor.id}`}
+ checked={vendorFlags[vendor.id.toString()]?.isCustomerPreferred || false}
+ onCheckedChange={(checked) =>
+ handleVendorFlagChange(vendor.id, 'isCustomerPreferred', checked as boolean)
+ }
+ />
+ <label
+ htmlFor={`customer-preferred-${vendor.id}`}
+ className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
+ >
+ 고객(선주) 선호 벤더
+ </label>
+ </div>
+
+ <div className="flex items-center space-x-2">
+ <Checkbox
+ id={`new-discovery-${vendor.id}`}
+ checked={vendorFlags[vendor.id.toString()]?.isNewDiscovery || false}
+ onCheckedChange={(checked) =>
+ handleVendorFlagChange(vendor.id, 'isNewDiscovery', checked as boolean)
+ }
+ />
+ <label
+ htmlFor={`new-discovery-${vendor.id}`}
+ className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
+ >
+ 신규 발굴 벤더
+ </label>
+ </div>
+ <div className="flex items-center space-x-2">
+ <Checkbox
+ id={`shi-proposal-${vendor.id}`}
+ checked={vendorFlags[vendor.id.toString()]?.isShiProposal || false}
+ onCheckedChange={(checked) =>
+ handleVendorFlagChange(vendor.id, 'isShiProposal', checked as boolean)
+ }
+ />
+ <label
+ htmlFor={`shi-proposal-${vendor.id}`}
+ className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
+ >
+ SHI Proposal Vendor
+ </label>
+ </div>
+ </>
+ )}
+
+ {/* 조선 RFQ가 아닌 경우: Project Approved Vendor, SHI Proposal Vendor 표시 */}
+ {!isShipRfq && (
+ <>
+ <div className="flex items-center space-x-2">
+ <Checkbox
+ id={`project-approved-${vendor.id}`}
+ checked={vendorFlags[vendor.id.toString()]?.isProjectApproved || false}
+ onCheckedChange={(checked) =>
+ handleVendorFlagChange(vendor.id, 'isProjectApproved', checked as boolean)
+ }
+ />
+ <label
+ htmlFor={`project-approved-${vendor.id}`}
+ className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
+ >
+ Project Approved Vendor
+ </label>
+ </div>
+
+ <div className="flex items-center space-x-2">
+ <Checkbox
+ id={`shi-proposal-${vendor.id}`}
+ checked={vendorFlags[vendor.id.toString()]?.isShiProposal || false}
+ onCheckedChange={(checked) =>
+ handleVendorFlagChange(vendor.id, 'isShiProposal', checked as boolean)
+ }
+ />
+ <label
+ htmlFor={`shi-proposal-${vendor.id}`}
+ className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
+ >
+ SHI Proposal Vendor
+ </label>
+ </div>
+ </>
+ )}
+ </div>
+ </CardContent>
+ </Card>
+ ))}
+ </div>
+ ) : (
+ <div className="text-center py-8 text-muted-foreground border rounded-md">
+ 선택된 벤더가 없습니다
+ </div>
+ )}
+ </div>
+ )
+ }
return (
<Dialog open={open} onOpenChange={onOpenChange}>
diff --git a/lib/techsales-rfq/table/detail-table/quotation-contacts-view-dialog.tsx b/lib/techsales-rfq/table/detail-table/quotation-contacts-view-dialog.tsx index 61c97b1b..608b5670 100644 --- a/lib/techsales-rfq/table/detail-table/quotation-contacts-view-dialog.tsx +++ b/lib/techsales-rfq/table/detail-table/quotation-contacts-view-dialog.tsx @@ -109,60 +109,62 @@ export function QuotationContactsViewDialog({ </div>
) : (
<div className="space-y-3">
- {contacts.map((contact) => (
- <div
- key={contact.id}
- className="flex items-center justify-between p-4 border rounded-lg bg-gray-50"
- >
- <div className="flex items-center gap-3">
- <User className="size-4 text-muted-foreground" />
- <div>
- <div className="flex items-center gap-2">
- <span className="font-medium">{contact.contactName}</span>
- {contact.isPrimary && (
- <Badge variant="secondary" className="text-xs">
- 주담당자
- </Badge>
+ {contacts
+ .filter((contact) => contact.contactTitle) // contactTitle이 있는 담당자만 필터링 (체크표시된 담당자)
+ .map((contact) => (
+ <div
+ key={contact.id}
+ className="flex items-center justify-between p-4 border rounded-lg bg-gray-50"
+ >
+ <div className="flex items-center gap-3">
+ <User className="size-4 text-muted-foreground" />
+ <div>
+ <div className="flex items-center gap-2">
+ <span className="font-medium">{contact.contactName}</span>
+ {contact.isPrimary && (
+ <Badge variant="secondary" className="text-xs">
+ 주담당자
+ </Badge>
+ )}
+ </div>
+ {contact.contactPosition && (
+ <p className="text-sm text-muted-foreground">
+ {contact.contactPosition}
+ </p>
+ )}
+ {contact.contactTitle && (
+ <p className="text-sm text-muted-foreground">
+ {contact.contactTitle}
+ </p>
+ )}
+ {contact.contactCountry && (
+ <p className="text-xs text-muted-foreground">
+ {contact.contactCountry}
+ </p>
)}
</div>
- {contact.contactPosition && (
- <p className="text-sm text-muted-foreground">
- {contact.contactPosition}
- </p>
- )}
- {contact.contactTitle && (
- <p className="text-sm text-muted-foreground">
- {contact.contactTitle}
- </p>
- )}
- {contact.contactCountry && (
- <p className="text-xs text-muted-foreground">
- {contact.contactCountry}
- </p>
- )}
- </div>
- </div>
-
- <div className="flex flex-col items-end gap-1 text-sm">
- <div className="flex items-center gap-1">
- <Mail className="size-4 text-muted-foreground" />
- <span>{contact.contactEmail}</span>
</div>
- {contact.contactPhone && (
+
+ <div className="flex flex-col items-end gap-1 text-sm">
<div className="flex items-center gap-1">
- <Phone className="size-4 text-muted-foreground" />
- <span>{contact.contactPhone}</span>
+ <Mail className="size-4 text-muted-foreground" />
+ <span>{contact.contactEmail}</span>
+ </div>
+ {contact.contactPhone && (
+ <div className="flex items-center gap-1">
+ <Phone className="size-4 text-muted-foreground" />
+ <span>{contact.contactPhone}</span>
+ </div>
+ )}
+ <div className="text-xs text-muted-foreground">
+ 발송일: {new Date(contact.createdAt).toLocaleDateString('ko-KR')}
</div>
- )}
- <div className="text-xs text-muted-foreground">
- 발송일: {new Date(contact.createdAt).toLocaleDateString('ko-KR')}
</div>
</div>
- </div>
- ))}
-
+ ))}
+
<div className="text-center pt-4 text-sm text-muted-foreground border-t">
- 총 {contacts.length}명의 담당자에게 발송됨
+ 총 {contacts.filter((contact) => contact.contactTitle).length}명의 담당자에게 발송됨
</div>
</div>
)}
diff --git a/lib/techsales-rfq/table/detail-table/quotation-history-dialog.tsx b/lib/techsales-rfq/table/detail-table/quotation-history-dialog.tsx index 7d972b91..023d3599 100644 --- a/lib/techsales-rfq/table/detail-table/quotation-history-dialog.tsx +++ b/lib/techsales-rfq/table/detail-table/quotation-history-dialog.tsx @@ -78,6 +78,7 @@ interface QuotationHistoryDialogProps { open: boolean
onOpenChange: (open: boolean) => void
quotationId: number | null
+ isInternal?: boolean // 내부 사용자인지 여부 (partners가 아니면 내부)
}
const statusConfig = {
@@ -88,15 +89,16 @@ const statusConfig = { "Rejected": { label: "거절됨", color: "bg-red-100 text-red-800" },
}
-function QuotationCard({
- data,
- version,
- isCurrent = false,
- revisedBy,
+function QuotationCard({
+ data,
+ version,
+ isCurrent = false,
+ revisedBy,
revisedAt,
attachments,
revisionId,
revisionNote,
+ isInternal = false,
}: {
data: QuotationSnapshot | QuotationHistoryData["current"]
version: number
@@ -106,6 +108,7 @@ function QuotationCard({ attachments?: QuotationAttachment[]
revisionId?: number
revisionNote?: string | null
+ isInternal?: boolean
}) {
const statusInfo = statusConfig[data.status as keyof typeof statusConfig] ||
{ label: data.status || "알 수 없음", color: "bg-gray-100 text-gray-800" }
@@ -171,7 +174,7 @@ function QuotationCard({ </div>
)}
- {revisionId && (
+ {revisionId && isInternal && (
<div>
<p className="text-sm font-medium text-muted-foreground mt-2">SHI Comment</p>
<textarea
@@ -241,10 +244,11 @@ function QuotationCard({ )
}
-export function QuotationHistoryDialog({
- open,
- onOpenChange,
- quotationId
+export function QuotationHistoryDialog({
+ open,
+ onOpenChange,
+ quotationId,
+ isInternal = false
}: QuotationHistoryDialogProps) {
const [data, setData] = useState<QuotationHistoryData | null>(null)
const [isLoading, setIsLoading] = useState(false)
@@ -312,6 +316,7 @@ export function QuotationHistoryDialog({ version={data.current.quotationVersion || 1}
isCurrent={true}
attachments={data.current.attachments}
+ isInternal={isInternal}
/>
{/* 이전 버전들 (스냅샷) - SHI Comment 포함 */}
@@ -326,6 +331,7 @@ export function QuotationHistoryDialog({ attachments={revision.attachments}
revisionId={revision.id}
revisionNote={revision.revisionNote}
+ isInternal={isInternal}
/>
))
) : (
diff --git a/lib/techsales-rfq/table/detail-table/rfq-detail-table.tsx b/lib/techsales-rfq/table/detail-table/rfq-detail-table.tsx index fe6f84e0..8ce55d56 100644 --- a/lib/techsales-rfq/table/detail-table/rfq-detail-table.tsx +++ b/lib/techsales-rfq/table/detail-table/rfq-detail-table.tsx @@ -781,6 +781,7 @@ export function RfqDetailTables({ selectedRfq, maxHeight }: RfqDetailTablesProps open={historyDialogOpen}
onOpenChange={setHistoryDialogOpen}
quotationId={selectedQuotationId}
+ isInternal={true}
/>
{/* 견적서 첨부파일 Sheet */}
diff --git a/lib/techsales-rfq/table/project-detail-dialog.tsx b/lib/techsales-rfq/table/project-detail-dialog.tsx index 00202501..b2a14efa 100644 --- a/lib/techsales-rfq/table/project-detail-dialog.tsx +++ b/lib/techsales-rfq/table/project-detail-dialog.tsx @@ -1,6 +1,7 @@ "use client"
import * as React from "react"
+import { toast } from "sonner"
import {
Dialog,
DialogContent,
@@ -10,6 +11,8 @@ import { } from "@/components/ui/dialog"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
+import { type ProjectSeries } from "@/db/schema/projects"
+import { getProjectSeriesForProject } from "@/lib/bidding-projects/service"
// 기본적인 RFQ 타입 정의 (rfq-table.tsx와 일치)
interface TechSalesRfq {
@@ -41,6 +44,25 @@ interface TechSalesRfq { quotationCount: number
}
+// K/L 날짜를 4분기로 변환하는 함수
+function convertKLToQuarter(klDate: string | null): string {
+ if (!klDate) return "정보 없음"
+
+ try {
+ // YYYYMMDD 형식의 날짜를 파싱
+ const year = parseInt(klDate.substring(0, 4))
+ const month = parseInt(klDate.substring(4, 6))
+
+ // 4분기 계산 (1-3월: 1Q, 4-6월: 2Q, 7-9월: 3Q, 10-12월: 4Q)
+ const quarter = Math.ceil(month / 3)
+
+ return `${year} ${quarter}Q`
+ } catch (error) {
+ console.error("K/L 날짜 변환 오류:", error)
+ return "날짜 오류"
+ }
+}
+
interface ProjectDetailDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
@@ -52,6 +74,30 @@ export function ProjectDetailDialog({ onOpenChange,
selectedRfq,
}: ProjectDetailDialogProps) {
+ const [projectSeries, setProjectSeries] = React.useState<ProjectSeries[]>([])
+ const [isLoadingSeries, setIsLoadingSeries] = React.useState(false)
+
+ React.useEffect(() => {
+ async function loadSeries() {
+ if (!selectedRfq?.pspid) return
+
+ setIsLoadingSeries(true)
+ try {
+ const result = await getProjectSeriesForProject(selectedRfq.pspid)
+ setProjectSeries(result)
+ } catch (error) {
+ console.error("프로젝트 시리즈 로드 오류:", error)
+ toast.error("프로젝트 시리즈 로드 실패")
+ } finally {
+ setIsLoadingSeries(false)
+ }
+ }
+
+ if (open && selectedRfq) {
+ loadSeries()
+ }
+ }, [selectedRfq, open])
+
if (!selectedRfq) {
return null
}
@@ -104,6 +150,47 @@ export function ProjectDetailDialog({ </div>
</div>
</div>
+
+ {/* 시리즈 정보 */}
+ <div className="space-y-4">
+ <h3 className="text-lg font-semibold">시리즈 정보</h3>
+ <div className="bg-gray-50 rounded-lg p-4">
+ {isLoadingSeries ? (
+ <div className="text-center py-4 text-gray-500">
+ 시리즈 정보 로딩 중...
+ </div>
+ ) : projectSeries.length > 0 ? (
+ <div className="grid grid-cols-1 gap-3">
+ {projectSeries.map((series) => (
+ <div key={series.sersNo} className="bg-white rounded border p-3">
+ <div className="grid grid-cols-1 md:grid-cols-4 gap-4 text-sm">
+ <div>
+ <span className="font-medium text-gray-700">시리즈번호:</span>
+ <div className="text-gray-900">{series.sersNo}</div>
+ </div>
+ <div>
+ <span className="font-medium text-gray-700">K/L (Keel Laying):</span>
+ <div className="text-gray-900">{convertKLToQuarter(series.klDt)}</div>
+ </div>
+ <div>
+ <span className="font-medium text-gray-700">도크코드:</span>
+ <div className="text-gray-900">{series.dockNo || "N/A"}</div>
+ </div>
+ <div>
+ <span className="font-medium text-gray-700">도크명:</span>
+ <div className="text-gray-900">{series.dockNm || "N/A"}</div>
+ </div>
+ </div>
+ </div>
+ ))}
+ </div>
+ ) : (
+ <div className="text-center py-4 text-gray-500">
+ 시리즈 데이터가 없습니다.
+ </div>
+ )}
+ </div>
+ </div>
</div>
{/* 닫기 버튼 */}
diff --git a/lib/techsales-rfq/table/update-rfq-sheet.tsx b/lib/techsales-rfq/table/update-rfq-sheet.tsx index 7dcc0e0e..f7bcbf9d 100644 --- a/lib/techsales-rfq/table/update-rfq-sheet.tsx +++ b/lib/techsales-rfq/table/update-rfq-sheet.tsx @@ -11,16 +11,20 @@ import { Loader2, CalendarIcon } from "lucide-react"; import { Button } from "@/components/ui/button";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
+import { Textarea } from "@/components/ui/textarea";
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetFooter, SheetClose } from "@/components/ui/sheet";
import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover";
import { Calendar } from "@/components/ui/calendar";
import { cn } from "@/lib/utils";
import { updateTechSalesRfq, getTechSalesRfqById } from "@/lib/techsales-rfq/service";
+import { getProjectSeriesForProject } from "@/lib/bidding-projects/service";
+import { type ProjectSeries } from "@/db/schema/projects";
// Zod schema for form validation
const updateRfqSchema = z.object({
rfqId: z.number().min(1, "RFQ ID is required"),
description: z.string(),
+ remark: z.string(),
dueDate: z.string(),
});
@@ -41,8 +45,39 @@ export default function UpdateSheet({ open, onOpenChange, rfqId, onUpdated }: Up projMsrm: "",
ptypeNm: "",
rfqNo: "",
+ pspid: "",
});
+ const [rfqInfo, setRfqInfo] = React.useState({
+ status: "",
+ createdAt: "",
+ updatedAt: "",
+ createdByName: "",
+ updatedByName: "",
+ sentByName: "",
+ rfqSendDate: "",
+ });
+ const [seriesInfo, setSeriesInfo] = React.useState<ProjectSeries[]>([]);
const [isLoading, setIsLoading] = React.useState(false);
+ const [isLoadingSeries, setIsLoadingSeries] = React.useState(false);
+
+ // K/L 날짜를 4분기로 변환하는 함수
+ const convertKLToQuarter = React.useCallback((klDate: string | null): string => {
+ if (!klDate) return "정보 없음"
+
+ try {
+ // YYYYMMDD 형식의 날짜를 파싱
+ const year = parseInt(klDate.substring(0, 4))
+ const month = parseInt(klDate.substring(4, 6))
+
+ // 4분기 계산 (1-3월: 1Q, 4-6월: 2Q, 7-9월: 3Q, 10-12월: 4Q)
+ const quarter = Math.ceil(month / 3)
+
+ return `${year} ${quarter}Q`
+ } catch (error) {
+ console.error("K/L 날짜 변환 오류:", error)
+ return "날짜 오류"
+ }
+ }, [])
// Initialize form with React Hook Form and Zod
const form = useForm<UpdateRfqSchema>({
@@ -50,6 +85,7 @@ export default function UpdateSheet({ open, onOpenChange, rfqId, onUpdated }: Up defaultValues: {
rfqId,
description: "",
+ remark: "",
dueDate: "",
},
});
@@ -74,15 +110,43 @@ export default function UpdateSheet({ open, onOpenChange, rfqId, onUpdated }: Up form.reset({
rfqId,
description: result.data.description || "",
+ remark: result.data.remark || "",
dueDate: result.data.dueDate ? new Date(result.data.dueDate).toISOString().slice(0, 10) : "",
});
+ const pspid = result.data.project[0].pspid || "";
setProjectInfo({
projNm: result.data.project[0].projectName || "",
sector: result.data.project[0].pjtType || "",
projMsrm: result.data.project[0].projMsrm || "",
ptypeNm: result.data.project[0].ptypeNm || "",
rfqNo: result.data.rfqCode || "",
+ pspid: pspid,
+ });
+ setRfqInfo({
+ status: result.data.status || "",
+ createdAt: result.data.createdAt ? format(new Date(result.data.createdAt), "yyyy-MM-dd HH:mm") : "",
+ updatedAt: result.data.updatedAt ? format(new Date(result.data.updatedAt), "yyyy-MM-dd HH:mm") : "",
+ createdByName: (result.data as any).createdByName || (result.data as any).createdBy || "",
+ updatedByName: (result.data as any).updatedByName || (result.data as any).updatedBy || "",
+ sentByName: (result.data as any).sentByName || (result.data as any).sentBy || "",
+ rfqSendDate: result.data.rfqSendDate ? format(new Date(result.data.rfqSendDate), "yyyy-MM-dd HH:mm") : "",
});
+
+ // 시리즈 정보 로드
+ if (pspid) {
+ setIsLoadingSeries(true);
+ try {
+ const seriesResult = await getProjectSeriesForProject(pspid);
+ setSeriesInfo(seriesResult);
+ } catch (error) {
+ console.error("시리즈 정보 로드 오류:", error);
+ setSeriesInfo([]);
+ } finally {
+ setIsLoadingSeries(false);
+ }
+ } else {
+ setSeriesInfo([]);
+ }
}
} catch (error: any) {
toast.error("RFQ 정보를 불러오는 중 오류가 발생했습니다: " + error.message);
@@ -101,6 +165,7 @@ export default function UpdateSheet({ open, onOpenChange, rfqId, onUpdated }: Up const result = await updateTechSalesRfq({
id: values.rfqId,
description: values.description,
+ remark: values.remark,
dueDate: new Date(values.dueDate),
updatedBy: 1, // Replace with actual user ID
});
@@ -134,9 +199,9 @@ export default function UpdateSheet({ open, onOpenChange, rfqId, onUpdated }: Up <Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent className="flex flex-col h-full sm:max-w-xl bg-gray-50">
<SheetHeader className="text-left flex-shrink-0">
- <SheetTitle className="text-2xl font-bold">RFQ 수정</SheetTitle>
+ <SheetTitle className="text-2xl font-bold">RFQ 미리보기</SheetTitle>
<SheetDescription className="">
- RFQ 정보를 수정합니다. 모든 필드를 입력한 후 저장 버튼을 클릭하세요.
+ RFQ 정보를 확인하고 필요한 항목을 수정하세요. RFQ Title, Context, 마감일만 수정 가능합니다.
</SheetDescription>
</SheetHeader>
@@ -147,7 +212,9 @@ export default function UpdateSheet({ open, onOpenChange, rfqId, onUpdated }: Up </div>
) : (
<div className="space-y-6">
+ {/* 프로젝트 정보 */}
<div className="bg-white shadow-sm rounded-lg p-5 border border-gray-200">
+ <h3 className="text-lg font-semibold mb-3 text-gray-800">프로젝트 정보</h3>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="font-semibold text-gray-700">프로젝트명:</span> {projectInfo.projNm}
@@ -167,6 +234,73 @@ export default function UpdateSheet({ open, onOpenChange, rfqId, onUpdated }: Up </div>
</div>
+ {/* 시리즈 정보 - 프로젝트 정보 아래에 배치 */}
+ <div className="bg-white shadow-sm rounded-lg p-5 border border-gray-200">
+ <h3 className="text-lg font-semibold mb-3 text-gray-800">시리즈 정보</h3>
+ {isLoadingSeries ? (
+ <div className="text-center py-4 text-gray-500">
+ 시리즈 정보 로딩 중...
+ </div>
+ ) : seriesInfo && seriesInfo.length > 0 ? (
+ <div className="grid grid-cols-1 gap-3">
+ {seriesInfo.map((series) => (
+ <div key={series.sersNo} className="bg-gray-50 rounded border p-3">
+ <div className="grid grid-cols-1 md:grid-cols-4 gap-4 text-sm">
+ <div>
+ <span className="font-medium text-gray-700">시리즈번호:</span>
+ <div className="text-gray-900">{series.sersNo}</div>
+ </div>
+ <div>
+ <span className="font-medium text-gray-700">K/L (Keel Laying):</span>
+ <div className="text-gray-900">{convertKLToQuarter(series.klDt)}</div>
+ </div>
+ <div>
+ <span className="font-medium text-gray-700">도크코드:</span>
+ <div className="text-gray-900">{series.dockNo || "N/A"}</div>
+ </div>
+ <div>
+ <span className="font-medium text-gray-700">도크명:</span>
+ <div className="text-gray-900">{series.dockNm || "N/A"}</div>
+ </div>
+ </div>
+ </div>
+ ))}
+ </div>
+ ) : (
+ <div className="text-center py-4 text-gray-500">
+ 시리즈 데이터가 없습니다.
+ </div>
+ )}
+ </div>
+
+ {/* RFQ 정보 */}
+ {/* <div className="bg-white shadow-sm rounded-lg p-5 border border-gray-200">
+ <h3 className="text-lg font-semibold mb-3 text-gray-800">RFQ 정보</h3>
+ <div className="grid grid-cols-2 gap-4 text-sm">
+ <div>
+ <span className="font-semibold text-gray-700">상태:</span> {rfqInfo.status}
+ </div>
+ <div>
+ <span className="font-semibold text-gray-700">생성자:</span> {rfqInfo.createdByName}
+ </div>
+ <div>
+ <span className="font-semibold text-gray-700">생성일:</span> {rfqInfo.createdAt}
+ </div>
+ <div>
+ <span className="font-semibold text-gray-700">수정자:</span> {rfqInfo.updatedByName}
+ </div>
+ <div>
+ <span className="font-semibold text-gray-700">수정일:</span> {rfqInfo.updatedAt}
+ </div>
+ <div>
+ <span className="font-semibold text-gray-700">발송자:</span> {rfqInfo.sentByName}
+ </div>
+ <div>
+ <span className="font-semibold text-gray-700">발송일:</span> {rfqInfo.rfqSendDate}
+ </div>
+ </div>
+ </div> */}
+
<Form {...form}>
<form id="update-rfq-form" onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-4">
<FormField
@@ -189,6 +323,24 @@ export default function UpdateSheet({ open, onOpenChange, rfqId, onUpdated }: Up <FormField
control={form.control}
+ name="remark"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel className="text-sm font-medium text-gray-700">RFQ Context</FormLabel>
+ <FormControl>
+ <Textarea
+ {...field}
+ placeholder="RFQ Context를 입력하세요"
+ className="border-gray-300 rounded-md min-h-[100px]"
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ <FormField
+ control={form.control}
name="dueDate"
render={({ field }) => (
<FormItem className="flex flex-col">
diff --git a/lib/techsales-rfq/vendor-response/detail/quotation-response-tab.tsx b/lib/techsales-rfq/vendor-response/detail/quotation-response-tab.tsx index 087e2a4d..afb491b8 100644 --- a/lib/techsales-rfq/vendor-response/detail/quotation-response-tab.tsx +++ b/lib/techsales-rfq/vendor-response/detail/quotation-response-tab.tsx @@ -517,6 +517,7 @@ export function QuotationResponseTab({ quotation }: QuotationResponseTabProps) { open={historyDialogOpen}
onOpenChange={setHistoryDialogOpen}
quotationId={quotation.id}
+ isInternal={false}
/>
</ScrollArea>
)
|
