summaryrefslogtreecommitdiff
path: root/lib/bidding/detail/table
diff options
context:
space:
mode:
Diffstat (limited to 'lib/bidding/detail/table')
-rw-r--r--lib/bidding/detail/table/bidding-award-dialog.tsx190
-rw-r--r--lib/bidding/detail/table/bidding-detail-vendor-table.tsx77
2 files changed, 247 insertions, 20 deletions
diff --git a/lib/bidding/detail/table/bidding-award-dialog.tsx b/lib/bidding/detail/table/bidding-award-dialog.tsx
index 9a4614bd..ff104fac 100644
--- a/lib/bidding/detail/table/bidding-award-dialog.tsx
+++ b/lib/bidding/detail/table/bidding-award-dialog.tsx
@@ -26,7 +26,8 @@ import {
} from '@/components/ui/table'
import { Trophy, Building2, Calculator } from 'lucide-react'
import { useToast } from '@/hooks/use-toast'
-import { getAwardedCompanies, awardBidding } from '@/lib/bidding/detail/service'
+import { getAwardedCompanies } from '@/lib/bidding/detail/service'
+import { requestBiddingAwardWithApproval } from '@/lib/bidding/approval-actions'
import { AwardSimpleFileUpload } from './components/award-simple-file-upload'
interface BiddingAwardDialogProps {
@@ -34,6 +35,12 @@ interface BiddingAwardDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
onSuccess: () => void
+ onApprovalPreview?: (data: {
+ templateName: string
+ variables: Record<string, string>
+ title: string
+ selectionReason: string
+ }) => void
}
interface AwardedCompany {
@@ -47,7 +54,8 @@ export function BiddingAwardDialog({
biddingId,
open,
onOpenChange,
- onSuccess
+ onSuccess,
+ onApprovalPreview
}: BiddingAwardDialogProps) {
const { toast } = useToast()
const { data: session } = useSession()
@@ -106,26 +114,36 @@ const userId = session?.user?.id || '2';
return
}
- startTransition(async () => {
- const result = await awardBidding(biddingId, selectionReason, userId)
+ // 결재 템플릿 변수 준비
+ const { mapBiddingAwardToTemplateVariables } = await import('@/lib/bidding/handlers')
- if (result.success) {
- toast({
- title: '성공',
- description: result.message,
- })
- onSuccess()
- onOpenChange(false)
- // 폼 초기화
- setSelectionReason('')
- } else {
- toast({
- title: '오류',
- description: result.error,
- variant: 'destructive',
+ try {
+ const variables = await mapBiddingAwardToTemplateVariables({
+ biddingId,
+ selectionReason,
+ requestedAt: new Date()
+ })
+
+ // 상위 컴포넌트로 결재 미리보기 데이터 전달
+ if (onApprovalPreview) {
+ onApprovalPreview({
+ templateName: '입찰 결과 업체 선정 품의 요청서',
+ variables,
+ title: `낙찰 - ${bidding?.title}`,
+ selectionReason
})
}
- })
+
+ onOpenChange(false)
+ setSelectionReason('')
+ } catch (error) {
+ console.error('낙찰 템플릿 변수 준비 실패:', error)
+ toast({
+ title: '오류',
+ description: '결재 문서 준비 중 오류가 발생했습니다.',
+ variant: 'destructive',
+ })
+ }
}
@@ -251,11 +269,143 @@ const userId = session?.user?.id || '2';
type="submit"
disabled={isPending || awardedCompanies.length === 0}
>
- {isPending ? '처리 중...' : '낙찰 완료'}
+ {isPending ? '상신 중...' : '결재 상신'}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
)
+
+ return (
+ <>
+ <Dialog open={open} onOpenChange={onOpenChange}>
+ <DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
+ <DialogHeader>
+ <DialogTitle className="flex items-center gap-2">
+ <Trophy className="w-5 h-5 text-yellow-600" />
+ 낙찰 처리
+ </DialogTitle>
+ <DialogDescription>
+ 낙찰된 업체의 발주비율과 선정 사유를 확인하고 낙찰을 완료하세요.
+ </DialogDescription>
+ </DialogHeader>
+
+ <form onSubmit={handleSubmit}>
+ <div className="space-y-6">
+ {/* 낙찰 업체 정보 */}
+ <Card>
+ <CardHeader>
+ <CardTitle className="flex items-center gap-2">
+ <Building2 className="w-4 h-4" />
+ 낙찰 업체 정보
+ </CardTitle>
+ </CardHeader>
+ <CardContent>
+ {isLoading ? (
+ <div className="text-center py-4">
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto"></div>
+ <p className="mt-2 text-sm text-muted-foreground">낙찰 업체 정보를 불러오는 중...</p>
+ </div>
+ ) : awardedCompanies.length > 0 ? (
+ <div className="space-y-4">
+ <Table>
+ <TableHeader>
+ <TableRow>
+ <TableHead>업체명</TableHead>
+ <TableHead className="text-right">견적금액</TableHead>
+ <TableHead className="text-right">발주비율</TableHead>
+ <TableHead className="text-right">발주금액</TableHead>
+ </TableRow>
+ </TableHeader>
+ <TableBody>
+ {awardedCompanies.map((company) => (
+ <TableRow key={company.companyId}>
+ <TableCell className="font-medium">
+ <div className="flex items-center gap-2">
+ <Badge variant="default" className="bg-green-600">낙찰</Badge>
+ {company.companyName}
+ </div>
+ </TableCell>
+ <TableCell className="text-right">
+ {company.finalQuoteAmount.toLocaleString()}원
+ </TableCell>
+ <TableCell className="text-right">
+ {company.awardRatio}%
+ </TableCell>
+ <TableCell className="text-right font-semibold">
+ {(company.finalQuoteAmount * company.awardRatio / 100).toLocaleString()}원
+ </TableCell>
+ </TableRow>
+ ))}
+ </TableBody>
+ </Table>
+
+ {/* 최종입찰가 요약 */}
+ <div className="flex items-center justify-between p-4 bg-blue-50 border border-blue-200 rounded-lg">
+ <div className="flex items-center gap-2">
+ <Calculator className="w-5 h-5 text-blue-600" />
+ <span className="font-semibold text-blue-800">최종입찰가</span>
+ </div>
+ <span className="text-xl font-bold text-blue-800">
+ {finalBidPrice.toLocaleString()}원
+ </span>
+ </div>
+ </div>
+ ) : (
+ <div className="text-center py-8">
+ <Trophy className="w-12 h-12 text-gray-400 mx-auto mb-4" />
+ <p className="text-gray-500 mb-2">낙찰된 업체가 없습니다</p>
+ <p className="text-sm text-gray-400">
+ 먼저 업체 수정 다이얼로그에서 발주비율을 산정해주세요.
+ </p>
+ </div>
+ )}
+ </CardContent>
+ </Card>
+
+ {/* 낙찰 사유 */}
+ <div className="space-y-2">
+ <Label htmlFor="selectionReason">
+ 낙찰 사유 <span className="text-red-500">*</span>
+ </Label>
+ <Textarea
+ id="selectionReason"
+ placeholder="낙찰 사유를 상세히 입력해주세요..."
+ value={selectionReason}
+ onChange={(e) => setSelectionReason(e.target.value)}
+ rows={4}
+ className="resize-none"
+ />
+ </div>
+
+ {/* 첨부파일 */}
+ <AwardSimpleFileUpload
+ biddingId={biddingId}
+ userId={userId}
+ readOnly={false}
+ />
+ </div>
+
+ <DialogFooter className="mt-6">
+ <Button
+ type="button"
+ variant="outline"
+ onClick={() => onOpenChange(false)}
+ disabled={isPending}
+ >
+ 취소
+ </Button>
+ <Button
+ type="submit"
+ disabled={isPending || awardedCompanies.length === 0}
+ >
+ {isPending ? '상신 중...' : '결재 상신'}
+ </Button>
+ </DialogFooter>
+ </form>
+ </DialogContent>
+ </Dialog>
+ </>
+ )
}
diff --git a/lib/bidding/detail/table/bidding-detail-vendor-table.tsx b/lib/bidding/detail/table/bidding-detail-vendor-table.tsx
index 1fa116ab..08fc0293 100644
--- a/lib/bidding/detail/table/bidding-detail-vendor-table.tsx
+++ b/lib/bidding/detail/table/bidding-detail-vendor-table.tsx
@@ -14,6 +14,8 @@ import { QuotationVendor, getPriceAdjustmentFormByBiddingCompanyId } from '@/lib
import { Bidding } from '@/db/schema'
import { PriceAdjustmentDialog } from '@/components/bidding/price-adjustment-dialog'
import { QuotationHistoryDialog } from './quotation-history-dialog'
+import { ApprovalPreviewDialog } from '@/lib/approval/approval-preview-dialog'
+import { requestBiddingAwardWithApproval } from '@/lib/bidding/approval-actions'
import { useToast } from '@/hooks/use-toast'
interface BiddingDetailVendorTableContentProps {
@@ -99,6 +101,13 @@ export function BiddingDetailVendorTableContent({
const [isPriceAdjustmentDialogOpen, setIsPriceAdjustmentDialogOpen] = React.useState(false)
const [quotationHistoryData, setQuotationHistoryData] = React.useState<any>(null)
const [isQuotationHistoryDialogOpen, setIsQuotationHistoryDialogOpen] = React.useState(false)
+ const [approvalPreviewData, setApprovalPreviewData] = React.useState<{
+ templateName: string
+ variables: Record<string, string>
+ title: string
+ selectionReason: string
+ } | null>(null)
+ const [isApprovalPreviewDialogOpen, setIsApprovalPreviewDialogOpen] = React.useState(false)
const handleEdit = (vendor: QuotationVendor) => {
setSelectedVendor(vendor)
@@ -187,6 +196,47 @@ export function BiddingDetailVendorTableContent({
clearOnDefault: true,
})
+ // 낙찰 결재 상신 핸들러
+ const handleAwardApprovalConfirm = async (data: { approvers: string[]; title: string; attachments?: File[] }) => {
+ if (!session?.user?.id || !approvalPreviewData) return
+
+ try {
+ const result = await requestBiddingAwardWithApproval({
+ biddingId,
+ selectionReason: approvalPreviewData.selectionReason,
+ currentUser: {
+ id: Number(session.user.id),
+ epId: session.user.epId || null,
+ email: session.user.email || undefined
+ },
+ approvers: data.approvers,
+ })
+
+ if (result.status === 'pending_approval') {
+ toast({
+ title: '성공',
+ description: `낙찰 결재가 상신되었습니다. (ID: ${result.approvalId})`,
+ })
+ setIsApprovalPreviewDialogOpen(false)
+ setApprovalPreviewData(null)
+ onRefresh()
+ } else {
+ toast({
+ title: '오류',
+ description: '낙찰 결재 상신 중 오류가 발생했습니다.',
+ variant: 'destructive',
+ })
+ }
+ } catch (error) {
+ console.error('낙찰 결재 상신 실패:', error)
+ toast({
+ title: '오류',
+ description: '낙찰 결재 상신 중 오류가 발생했습니다.',
+ variant: 'destructive',
+ })
+ }
+ }
+
return (
<>
<DataTable table={table}>
@@ -221,6 +271,10 @@ export function BiddingDetailVendorTableContent({
open={isAwardDialogOpen}
onOpenChange={setIsAwardDialogOpen}
onSuccess={onRefresh}
+ onApprovalPreview={(data) => {
+ setApprovalPreviewData(data)
+ setIsApprovalPreviewDialogOpen(true)
+ }}
/>
<PriceAdjustmentDialog
@@ -238,6 +292,29 @@ export function BiddingDetailVendorTableContent({
biddingCurrency={quotationHistoryData?.biddingCurrency || 'KRW'}
targetPrice={quotationHistoryData?.targetPrice}
/>
+
+ {/* 낙찰 결재 미리보기 다이얼로그 */}
+ {session?.user && session.user.epId && approvalPreviewData && (
+ <ApprovalPreviewDialog
+ open={isApprovalPreviewDialogOpen}
+ onOpenChange={(open) => {
+ setIsApprovalPreviewDialogOpen(open)
+ if (!open) {
+ setApprovalPreviewData(null)
+ }
+ }}
+ templateName={approvalPreviewData.templateName}
+ variables={approvalPreviewData.variables}
+ title={approvalPreviewData.title}
+ currentUser={{
+ id: Number(session.user.id),
+ epId: session.user.epId,
+ name: session.user.name || undefined,
+ email: session.user.email || undefined
+ }}
+ onConfirm={handleAwardApprovalConfirm}
+ />
+ )}
</>
)
}