tenantId(); $query = Bidding::where('tenant_id', $tenantId) ->with(['quote:id,quote_number']); // 상태 필터 if (! empty($params['status'])) { $query->where('status', $params['status']); } // 날짜 범위 필터 if (! empty($params['from_date'])) { $query->where('bidding_date', '>=', $params['from_date']); } if (! empty($params['to_date'])) { $query->where('bidding_date', '<=', $params['to_date']); } // 검색 (입찰번호, 거래처명, 현장명, 입찰자명) if (! empty($params['search'])) { $keyword = $params['search']; $query->where(function ($q) use ($keyword) { $q->where('bidding_code', 'like', "%{$keyword}%") ->orWhere('client_name', 'like', "%{$keyword}%") ->orWhere('project_name', 'like', "%{$keyword}%") ->orWhere('bidder_name', 'like', "%{$keyword}%"); }); } // 정렬 $sortBy = $params['sort_by'] ?? 'bidding_date'; $sortDir = $params['sort_dir'] ?? 'desc'; $query->orderBy($sortBy, $sortDir); // 페이지네이션 (최대 100개로 제한) $perPage = min($params['per_page'] ?? 20, 100); return $query->paginate($perPage); } /** * 입찰 생성 (견적에서 전환 포함) */ public function store(array $data): Bidding { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); // 견적에서 전환하는 경우, 이미 입찰이 등록되었는지 체크 if (! empty($data['quote_id'])) { $existingBidding = Bidding::where('tenant_id', $tenantId) ->where('quote_id', $data['quote_id']) ->first(); if ($existingBidding) { throw new \Symfony\Component\HttpKernel\Exception\ConflictHttpException( __('error.bidding.already_registered', ['code' => $existingBidding->bidding_code]) ); } } // 입찰번호 자동 생성 $biddingCode = $this->generateBiddingCode($tenantId); $bidding = Bidding::create([ 'tenant_id' => $tenantId, 'bidding_code' => $biddingCode, 'quote_id' => $data['quote_id'] ?? null, // 거래처/현장 'client_id' => $data['client_id'] ?? null, 'client_name' => $data['client_name'] ?? null, 'project_name' => $data['project_name'], // 입찰 정보 'bidding_date' => $data['bidding_date'] ?? now()->toDateString(), 'bid_date' => $data['bid_date'] ?? null, 'submission_date' => $data['submission_date'] ?? null, 'confirm_date' => $data['confirm_date'] ?? null, 'total_count' => $data['total_count'] ?? 0, 'bidding_amount' => $data['bidding_amount'] ?? 0, // 상태 (기본값: waiting) 'status' => $data['status'] ?? Bidding::STATUS_WAITING, // 입찰자 'bidder_id' => $data['bidder_id'] ?? null, 'bidder_name' => $data['bidder_name'] ?? null, // 공사기간 'construction_start_date' => $data['construction_start_date'] ?? null, 'construction_end_date' => $data['construction_end_date'] ?? null, 'vat_type' => $data['vat_type'] ?? Bidding::VAT_EXCLUDED, // 비고 'remarks' => $data['remarks'] ?? null, // 견적 데이터 스냅샷 'expense_items' => $data['expense_items'] ?? null, 'estimate_detail_items' => $data['estimate_detail_items'] ?? null, // 감사 'created_by' => $userId, ]); return $bidding->load(['quote:id,quote_number']); } /** * 입찰번호 자동 생성 (BID-YYYY-NNN) */ private function generateBiddingCode(int $tenantId): string { $year = now()->year; $prefix = "BID-{$year}-"; // 올해 생성된 마지막 입찰번호 조회 $lastBidding = Bidding::where('tenant_id', $tenantId) ->where('bidding_code', 'like', "{$prefix}%") ->orderBy('bidding_code', 'desc') ->first(); if ($lastBidding) { // 마지막 번호에서 숫자 추출 후 +1 $lastNumber = (int) substr($lastBidding->bidding_code, -3); $nextNumber = $lastNumber + 1; } else { $nextNumber = 1; } return $prefix.str_pad($nextNumber, 3, '0', STR_PAD_LEFT); } /** * 입찰 통계 조회 */ public function stats(): array { $tenantId = $this->tenantId(); return [ 'total' => Bidding::where('tenant_id', $tenantId)->count(), 'waiting' => Bidding::where('tenant_id', $tenantId)->where('status', Bidding::STATUS_WAITING)->count(), 'submitted' => Bidding::where('tenant_id', $tenantId)->where('status', Bidding::STATUS_SUBMITTED)->count(), 'awarded' => Bidding::where('tenant_id', $tenantId)->where('status', Bidding::STATUS_AWARDED)->count(), 'failed' => Bidding::where('tenant_id', $tenantId)->where('status', Bidding::STATUS_FAILED)->count(), 'invalid' => Bidding::where('tenant_id', $tenantId)->where('status', Bidding::STATUS_INVALID)->count(), 'hold' => Bidding::where('tenant_id', $tenantId)->where('status', Bidding::STATUS_HOLD)->count(), ]; } /** * 입찰 단건 조회 */ public function show(int $id): Bidding { $tenantId = $this->tenantId(); $bidding = Bidding::where('tenant_id', $tenantId) ->with(['quote:id,quote_number,site_name,total_amount,status']) ->find($id); if (! $bidding) { throw new NotFoundHttpException(__('error.not_found')); } return $bidding; } /** * 입찰 수정 */ public function update(int $id, array $data): Bidding { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); $bidding = Bidding::where('tenant_id', $tenantId)->find($id); if (! $bidding) { throw new NotFoundHttpException(__('error.not_found')); } $data['updated_by'] = $userId; $bidding->update($data); return $bidding->fresh(['quote:id,quote_number']); } /** * 입찰 삭제 (소프트 삭제) */ public function destroy(int $id): bool { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); $bidding = Bidding::where('tenant_id', $tenantId)->find($id); if (! $bidding) { throw new NotFoundHttpException(__('error.not_found')); } $bidding->update(['deleted_by' => $userId]); $bidding->delete(); return true; } /** * 입찰 일괄 삭제 */ public function bulkDestroy(array $ids): int { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); $biddings = Bidding::where('tenant_id', $tenantId) ->whereIn('id', $ids) ->get(); $deletedCount = 0; foreach ($biddings as $bidding) { $bidding->update(['deleted_by' => $userId]); $bidding->delete(); $deletedCount++; } return $deletedCount; } /** * 입찰 상태 변경 */ public function updateStatus(int $id, string $status): Bidding { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); $bidding = Bidding::where('tenant_id', $tenantId)->find($id); if (! $bidding) { throw new NotFoundHttpException(__('error.not_found')); } $bidding->update([ 'status' => $status, 'updated_by' => $userId, ]); return $bidding->fresh(['quote:id,quote_number']); } }