diff --git a/app/Services/AdminFcmService.php b/app/Services/AdminFcmService.php index 54a3d51..cbe1b6b 100644 --- a/app/Services/AdminFcmService.php +++ b/app/Services/AdminFcmService.php @@ -261,4 +261,4 @@ public function getHistory(array $filters, int $perPage = 20): array ->paginate($perPage) ->toArray(); } -} \ No newline at end of file +} diff --git a/app/Services/Authz/RoleService.php b/app/Services/Authz/RoleService.php index 7814329..5cce42b 100644 --- a/app/Services/Authz/RoleService.php +++ b/app/Services/Authz/RoleService.php @@ -2,10 +2,10 @@ namespace App\Services\Authz; +use App\Models\Permissions\Role; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Validator; use Illuminate\Validation\Rule; -use App\Models\Permissions\Role; use Spatie\Permission\PermissionRegistrar; class RoleService diff --git a/app/Services/Construction/HandoverReportService.php b/app/Services/Construction/HandoverReportService.php new file mode 100644 index 0000000..d636a0d --- /dev/null +++ b/app/Services/Construction/HandoverReportService.php @@ -0,0 +1,333 @@ +tenantId(); + + $query = HandoverReport::query() + ->where('tenant_id', $tenantId); + + // 검색 필터 + if (! empty($params['search'])) { + $search = $params['search']; + $query->where(function ($q) use ($search) { + $q->where('report_number', 'like', "%{$search}%") + ->orWhere('site_name', 'like', "%{$search}%") + ->orWhere('partner_name', 'like', "%{$search}%"); + }); + } + + // 상태 필터 + if (! empty($params['status'])) { + $query->where('status', $params['status']); + } + + // 거래처 필터 + if (! empty($params['partner_id'])) { + $query->where('partner_id', $params['partner_id']); + } + + // 계약담당자 필터 + if (! empty($params['contract_manager_id'])) { + $query->where('contract_manager_id', $params['contract_manager_id']); + } + + // 공사PM 필터 + if (! empty($params['construction_pm_id'])) { + $query->where('construction_pm_id', $params['construction_pm_id']); + } + + // 연결 계약 필터 + if (! empty($params['contract_id'])) { + $query->where('contract_id', $params['contract_id']); + } + + // 날짜 범위 필터 + if (! empty($params['start_date'])) { + $query->where('contract_start_date', '>=', $params['start_date']); + } + if (! empty($params['end_date'])) { + $query->where('contract_end_date', '<=', $params['end_date']); + } + + // 활성화 상태 필터 + if (isset($params['is_active'])) { + $query->where('is_active', $params['is_active']); + } + + // 정렬 + $sortBy = $params['sort_by'] ?? 'created_at'; + $sortDir = $params['sort_dir'] ?? 'desc'; + $query->orderBy($sortBy, $sortDir); + + // 페이지네이션 + $perPage = $params['per_page'] ?? 20; + + return $query->paginate($perPage); + } + + /** + * 인수인계보고서 상세 조회 + */ + public function show(int $id): HandoverReport + { + $tenantId = $this->tenantId(); + + return HandoverReport::query() + ->where('tenant_id', $tenantId) + ->with(['contract', 'contractManager', 'constructionPm', 'managers', 'items', 'creator', 'updater']) + ->findOrFail($id); + } + + /** + * 인수인계보고서 등록 + */ + public function store(array $data): HandoverReport + { + $tenantId = $this->tenantId(); + $userId = $this->apiUserId(); + + return DB::transaction(function () use ($data, $tenantId, $userId) { + // 메인 보고서 생성 + $report = HandoverReport::create([ + 'tenant_id' => $tenantId, + 'report_number' => $data['report_number'], + 'contract_id' => $data['contract_id'] ?? null, + 'site_name' => $data['site_name'], + 'partner_id' => $data['partner_id'] ?? null, + 'partner_name' => $data['partner_name'] ?? null, + 'contract_manager_id' => $data['contract_manager_id'] ?? null, + 'contract_manager_name' => $data['contract_manager_name'] ?? null, + 'construction_pm_id' => $data['construction_pm_id'] ?? null, + 'construction_pm_name' => $data['construction_pm_name'] ?? null, + 'total_sites' => $data['total_sites'] ?? 0, + 'contract_amount' => $data['contract_amount'] ?? 0, + 'contract_date' => $data['contract_date'] ?? null, + 'contract_start_date' => $data['contract_start_date'] ?? null, + 'contract_end_date' => $data['contract_end_date'] ?? null, + 'completion_date' => $data['completion_date'] ?? null, + 'status' => $data['status'] ?? HandoverReport::STATUS_PENDING, + 'has_secondary_piping' => $data['has_secondary_piping'] ?? false, + 'secondary_piping_amount' => $data['secondary_piping_amount'] ?? 0, + 'secondary_piping_note' => $data['secondary_piping_note'] ?? null, + 'has_coating' => $data['has_coating'] ?? false, + 'coating_amount' => $data['coating_amount'] ?? 0, + 'coating_note' => $data['coating_note'] ?? null, + 'external_equipment_cost' => $data['external_equipment_cost'] ?? null, + 'special_notes' => $data['special_notes'] ?? null, + 'is_active' => $data['is_active'] ?? true, + 'created_by' => $userId, + 'updated_by' => $userId, + ]); + + // 공사담당자 생성 + if (! empty($data['managers'])) { + $this->syncManagers($report, $data['managers'], $tenantId, $userId); + } + + // 계약 ITEM 생성 + if (! empty($data['items'])) { + $this->syncItems($report, $data['items'], $tenantId, $userId); + } + + return $report->load(['managers', 'items']); + }); + } + + /** + * 인수인계보고서 수정 + */ + public function update(int $id, array $data): HandoverReport + { + $tenantId = $this->tenantId(); + $userId = $this->apiUserId(); + + return DB::transaction(function () use ($id, $data, $tenantId, $userId) { + $report = HandoverReport::query() + ->where('tenant_id', $tenantId) + ->findOrFail($id); + + $report->fill([ + 'report_number' => $data['report_number'] ?? $report->report_number, + 'contract_id' => $data['contract_id'] ?? $report->contract_id, + 'site_name' => $data['site_name'] ?? $report->site_name, + 'partner_id' => $data['partner_id'] ?? $report->partner_id, + 'partner_name' => $data['partner_name'] ?? $report->partner_name, + 'contract_manager_id' => $data['contract_manager_id'] ?? $report->contract_manager_id, + 'contract_manager_name' => $data['contract_manager_name'] ?? $report->contract_manager_name, + 'construction_pm_id' => $data['construction_pm_id'] ?? $report->construction_pm_id, + 'construction_pm_name' => $data['construction_pm_name'] ?? $report->construction_pm_name, + 'total_sites' => $data['total_sites'] ?? $report->total_sites, + 'contract_amount' => $data['contract_amount'] ?? $report->contract_amount, + 'contract_date' => $data['contract_date'] ?? $report->contract_date, + 'contract_start_date' => $data['contract_start_date'] ?? $report->contract_start_date, + 'contract_end_date' => $data['contract_end_date'] ?? $report->contract_end_date, + 'completion_date' => $data['completion_date'] ?? $report->completion_date, + 'status' => $data['status'] ?? $report->status, + 'has_secondary_piping' => $data['has_secondary_piping'] ?? $report->has_secondary_piping, + 'secondary_piping_amount' => $data['secondary_piping_amount'] ?? $report->secondary_piping_amount, + 'secondary_piping_note' => $data['secondary_piping_note'] ?? $report->secondary_piping_note, + 'has_coating' => $data['has_coating'] ?? $report->has_coating, + 'coating_amount' => $data['coating_amount'] ?? $report->coating_amount, + 'coating_note' => $data['coating_note'] ?? $report->coating_note, + 'external_equipment_cost' => $data['external_equipment_cost'] ?? $report->external_equipment_cost, + 'special_notes' => $data['special_notes'] ?? $report->special_notes, + 'is_active' => $data['is_active'] ?? $report->is_active, + 'updated_by' => $userId, + ]); + + $report->save(); + + // 공사담당자 동기화 + if (array_key_exists('managers', $data)) { + $this->syncManagers($report, $data['managers'] ?? [], $tenantId, $userId); + } + + // 계약 ITEM 동기화 + if (array_key_exists('items', $data)) { + $this->syncItems($report, $data['items'] ?? [], $tenantId, $userId); + } + + return $report->fresh(['managers', 'items']); + }); + } + + /** + * 인수인계보고서 삭제 + */ + public function destroy(int $id): bool + { + $tenantId = $this->tenantId(); + $userId = $this->apiUserId(); + + return DB::transaction(function () use ($id, $tenantId, $userId) { + $report = HandoverReport::query() + ->where('tenant_id', $tenantId) + ->findOrFail($id); + + $report->deleted_by = $userId; + $report->save(); + $report->delete(); + + return true; + }); + } + + /** + * 인수인계보고서 일괄 삭제 + */ + public function bulkDestroy(array $ids): bool + { + $tenantId = $this->tenantId(); + $userId = $this->apiUserId(); + + return DB::transaction(function () use ($ids, $tenantId, $userId) { + $reports = HandoverReport::query() + ->where('tenant_id', $tenantId) + ->whereIn('id', $ids) + ->get(); + + foreach ($reports as $report) { + $report->deleted_by = $userId; + $report->save(); + $report->delete(); + } + + return true; + }); + } + + /** + * 인수인계보고서 통계 조회 + */ + public function stats(array $params): array + { + $tenantId = $this->tenantId(); + + $query = HandoverReport::query() + ->where('tenant_id', $tenantId); + + // 날짜 범위 필터 + if (! empty($params['start_date'])) { + $query->where('contract_start_date', '>=', $params['start_date']); + } + if (! empty($params['end_date'])) { + $query->where('contract_end_date', '<=', $params['end_date']); + } + + $totalCount = (clone $query)->count(); + $pendingCount = (clone $query)->where('status', HandoverReport::STATUS_PENDING)->count(); + $completedCount = (clone $query)->where('status', HandoverReport::STATUS_COMPLETED)->count(); + $totalAmount = (clone $query)->sum('contract_amount'); + $totalSites = (clone $query)->sum('total_sites'); + + return [ + 'total_count' => $totalCount, + 'pending_count' => $pendingCount, + 'completed_count' => $completedCount, + 'total_amount' => (float) $totalAmount, + 'total_sites' => (int) $totalSites, + ]; + } + + /** + * 공사담당자 동기화 + */ + private function syncManagers(HandoverReport $report, array $managers, int $tenantId, int $userId): void + { + // 기존 담당자 삭제 + $report->managers()->delete(); + + // 새 담당자 생성 + foreach ($managers as $index => $manager) { + HandoverReportManager::create([ + 'tenant_id' => $tenantId, + 'handover_report_id' => $report->id, + 'name' => $manager['name'], + 'non_performance_reason' => $manager['non_performance_reason'] ?? null, + 'signature' => $manager['signature'] ?? null, + 'sort_order' => $index, + 'created_by' => $userId, + 'updated_by' => $userId, + ]); + } + } + + /** + * 계약 ITEM 동기화 + */ + private function syncItems(HandoverReport $report, array $items, int $tenantId, int $userId): void + { + // 기존 아이템 삭제 + $report->items()->delete(); + + // 새 아이템 생성 + foreach ($items as $index => $item) { + HandoverReportItem::create([ + 'tenant_id' => $tenantId, + 'handover_report_id' => $report->id, + 'item_no' => $item['item_no'] ?? ($index + 1), + 'name' => $item['name'], + 'product' => $item['product'] ?? null, + 'quantity' => $item['quantity'] ?? 0, + 'remark' => $item['remark'] ?? null, + 'created_by' => $userId, + 'updated_by' => $userId, + ]); + } + } +} diff --git a/app/Services/Construction/StructureReviewService.php b/app/Services/Construction/StructureReviewService.php index d763364..2ede0b1 100644 --- a/app/Services/Construction/StructureReviewService.php +++ b/app/Services/Construction/StructureReviewService.php @@ -226,4 +226,4 @@ public function stats(array $params): array 'completed' => $completedCount, ]; } -} \ No newline at end of file +} diff --git a/app/Services/DailyReportService.php b/app/Services/DailyReportService.php index 252956e..1240ea6 100644 --- a/app/Services/DailyReportService.php +++ b/app/Services/DailyReportService.php @@ -7,7 +7,6 @@ use App\Models\Tenants\Deposit; use App\Models\Tenants\Withdrawal; use Carbon\Carbon; -use Illuminate\Support\Facades\DB; /** * 일일 보고서 서비스 diff --git a/app/Services/Estimate/EstimateService.php b/app/Services/Estimate/EstimateService.php index 6a28bba..e2047fd 100644 --- a/app/Services/Estimate/EstimateService.php +++ b/app/Services/Estimate/EstimateService.php @@ -24,7 +24,6 @@ public function __construct( CalculationEngine $calculationEngine, PricingService $pricingService ) { - parent::__construct(); $this->modelSetService = $modelSetService; $this->calculationEngine = $calculationEngine; $this->pricingService = $pricingService; @@ -362,6 +361,37 @@ protected function createEstimateItems(Estimate $estimate, array $bomItems, ?int return $totalAmount; } + /** + * 견적 통계 조회 + */ + public function stats(): array + { + $tenantId = $this->tenantId(); + + $stats = Estimate::query() + ->where('tenant_id', $tenantId) + ->selectRaw(" + COUNT(*) as total, + SUM(CASE WHEN status = 'DRAFT' THEN 1 ELSE 0 END) as draft, + SUM(CASE WHEN status = 'SENT' THEN 1 ELSE 0 END) as sent, + SUM(CASE WHEN status = 'APPROVED' THEN 1 ELSE 0 END) as approved, + SUM(CASE WHEN status = 'REJECTED' THEN 1 ELSE 0 END) as rejected, + SUM(CASE WHEN status = 'EXPIRED' THEN 1 ELSE 0 END) as expired, + SUM(total_amount) as total_amount + ") + ->first(); + + return [ + 'total' => (int) $stats->total, + 'draft' => (int) $stats->draft, + 'sent' => (int) $stats->sent, + 'approved' => (int) $stats->approved, + 'rejected' => (int) $stats->rejected, + 'expired' => (int) $stats->expired, + 'total_amount' => (float) ($stats->total_amount ?? 0), + ]; + } + /** * 계산 결과 요약 */ diff --git a/app/Services/ItemService.php b/app/Services/ItemService.php index fd56fdb..f55ddca 100644 --- a/app/Services/ItemService.php +++ b/app/Services/ItemService.php @@ -933,4 +933,42 @@ private function groupFilesByFieldKey(array $files): array return $grouped; } + + /** + * 품목 통계 조회 + * + * @param array $params 검색 파라미터 (item_type 또는 group_id, 없으면 전체) + * @return array{total: int, active: int} + */ + public function stats(array $params = []): array + { + $tenantId = $this->tenantId(); + $itemType = $params['item_type'] ?? null; + $groupId = $params['group_id'] ?? null; + + // 기본 쿼리 (items 테이블) + $baseQuery = Item::where('tenant_id', $tenantId); + + // item_type 필터 + if ($itemType) { + $itemTypes = $this->parseItemTypes($itemType); + if (! empty($itemTypes)) { + $baseQuery->whereIn('item_type', $itemTypes); + } + } elseif ($groupId) { + // group_id로 해당 그룹의 item_type 조회 + $itemTypes = $this->getItemTypesByGroupId((int) $groupId); + if (! empty($itemTypes)) { + $baseQuery->whereIn('item_type', $itemTypes); + } + } + + $total = (clone $baseQuery)->count(); + $active = (clone $baseQuery)->where('is_active', true)->count(); + + return [ + 'total' => $total, + 'active' => $active, + ]; + } } diff --git a/app/Services/ModelSet/ModelSetService.php b/app/Services/ModelSet/ModelSetService.php index 3e20359..d356d08 100644 --- a/app/Services/ModelSet/ModelSetService.php +++ b/app/Services/ModelSet/ModelSetService.php @@ -19,7 +19,6 @@ class ModelSetService extends Service public function __construct(CalculationEngine $calculationEngine) { - parent::__construct(); $this->calculationEngine = $calculationEngine; } diff --git a/app/Services/PositionService.php b/app/Services/PositionService.php index 854f75e..de943f9 100644 --- a/app/Services/PositionService.php +++ b/app/Services/PositionService.php @@ -138,4 +138,4 @@ public function reorder(array $items) return ['success' => true, 'updated' => count($items)]; } -} \ No newline at end of file +} diff --git a/app/Services/Pricing/PricingService.php b/app/Services/Pricing/PricingService.php new file mode 100644 index 0000000..ba8eb42 --- /dev/null +++ b/app/Services/Pricing/PricingService.php @@ -0,0 +1,31 @@ + 0, + 'warning' => null, + ]; + } +} diff --git a/app/Services/PushNotificationService.php b/app/Services/PushNotificationService.php index f77375e..d5f858a 100644 --- a/app/Services/PushNotificationService.php +++ b/app/Services/PushNotificationService.php @@ -404,4 +404,4 @@ private function getChannelForEvent(string $event): string default => 'push_default', }; } -} \ No newline at end of file +} diff --git a/app/Services/Quote/QuoteService.php b/app/Services/Quote/QuoteService.php index 7afaa29..be8aa0b 100644 --- a/app/Services/Quote/QuoteService.php +++ b/app/Services/Quote/QuoteService.php @@ -96,7 +96,7 @@ public function show(int $id): Quote { $tenantId = $this->tenantId(); - $quote = Quote::with(['items', 'revisions', 'client', 'creator', 'updater', 'finalizer']) + $quote = Quote::with(['items', 'revisions', 'client', 'creator', 'updater', 'finalizer', 'siteBriefing.partner']) ->where('tenant_id', $tenantId) ->find($id); diff --git a/app/Services/ReceivablesService.php b/app/Services/ReceivablesService.php index bef1ab5..3c7b2c9 100644 --- a/app/Services/ReceivablesService.php +++ b/app/Services/ReceivablesService.php @@ -7,7 +7,6 @@ use App\Models\Tenants\Deposit; use App\Models\Tenants\Sale; use Carbon\Carbon; -use Illuminate\Support\Facades\DB; /** * 채권 현황 서비스 @@ -183,6 +182,7 @@ public function summary(array $params): array /** * 월 기간 배열 생성 + * * @return array [['start' => 'Y-m-d', 'end' => 'Y-m-d', 'label' => 'YY.MM', 'year' => Y, 'month' => M], ...] */ private function generateMonthPeriods(bool $recentYear, string $year): array @@ -449,4 +449,4 @@ public function updateMemos(array $memos): int return $updatedCount; } -} \ No newline at end of file +} diff --git a/app/Services/SalaryService.php b/app/Services/SalaryService.php index c3fa0e2..9ddb47a 100644 --- a/app/Services/SalaryService.php +++ b/app/Services/SalaryService.php @@ -19,12 +19,12 @@ public function index(array $params): LengthAwarePaginator ->where('tenant_id', $tenantId) ->with([ 'employee:id,name,user_id,email', - 'employeeProfile' => fn($q) => $q->where('tenant_id', $tenantId), + 'employeeProfile' => fn ($q) => $q->where('tenant_id', $tenantId), 'employeeProfile.department:id,name', ]); // 검색 필터 (직원명) - if (!empty($params['search'])) { + if (! empty($params['search'])) { $search = $params['search']; $query->whereHas('employee', function ($q) use ($search) { $q->where('name', 'like', "%{$search}%"); @@ -32,27 +32,27 @@ public function index(array $params): LengthAwarePaginator } // 연도 필터 - if (!empty($params['year'])) { + if (! empty($params['year'])) { $query->where('year', $params['year']); } // 월 필터 - if (!empty($params['month'])) { + if (! empty($params['month'])) { $query->where('month', $params['month']); } // 상태 필터 - if (!empty($params['status'])) { + if (! empty($params['status'])) { $query->where('status', $params['status']); } // 기간 필터 - if (!empty($params['start_date']) && !empty($params['end_date'])) { + if (! empty($params['start_date']) && ! empty($params['end_date'])) { $query->whereBetween('payment_date', [$params['start_date'], $params['end_date']]); } // 직원 ID 필터 - if (!empty($params['employee_id'])) { + if (! empty($params['employee_id'])) { $query->where('employee_id', $params['employee_id']); } @@ -84,7 +84,7 @@ public function show(int $id): Salary ->where('tenant_id', $tenantId) ->with([ 'employee:id,name,user_id,email', - 'employeeProfile' => fn($q) => $q->where('tenant_id', $tenantId), + 'employeeProfile' => fn ($q) => $q->where('tenant_id', $tenantId), 'employeeProfile.department:id,name', ]) ->findOrFail($id); @@ -183,7 +183,7 @@ public function update(int $id, array $data): Salary return $salary->fresh()->load([ 'employee:id,name,user_id,email', - 'employeeProfile' => fn($q) => $q->where('tenant_id', $tenantId), + 'employeeProfile' => fn ($q) => $q->where('tenant_id', $tenantId), 'employeeProfile.department:id,name', ]); }); @@ -229,7 +229,7 @@ public function updateStatus(int $id, string $status): Salary return $salary->load([ 'employee:id,name,user_id,email', - 'employeeProfile' => fn($q) => $q->where('tenant_id', $tenantId), + 'employeeProfile' => fn ($q) => $q->where('tenant_id', $tenantId), 'employeeProfile.department:id,name', ]); }); @@ -266,15 +266,15 @@ public function getStatistics(array $params): array ->where('tenant_id', $tenantId); // 연도/월 필터 - if (!empty($params['year'])) { + if (! empty($params['year'])) { $query->where('year', $params['year']); } - if (!empty($params['month'])) { + if (! empty($params['month'])) { $query->where('month', $params['month']); } // 기간 필터 - if (!empty($params['start_date']) && !empty($params['end_date'])) { + if (! empty($params['start_date']) && ! empty($params['end_date'])) { $query->whereBetween('payment_date', [$params['start_date'], $params['end_date']]); } @@ -290,4 +290,4 @@ public function getStatistics(array $params): array 'scheduled_count' => (clone $query)->where('status', 'scheduled')->count(), ]; } -} \ No newline at end of file +} diff --git a/app/Services/SiteBriefingService.php b/app/Services/SiteBriefingService.php new file mode 100644 index 0000000..c774a3e --- /dev/null +++ b/app/Services/SiteBriefingService.php @@ -0,0 +1,268 @@ +tenantId(); + + $query = SiteBriefing::query() + ->with(['partner:id,name', 'site:id,name']) + ->where('tenant_id', $tenantId); + + // 검색 + if (! empty($params['search'])) { + $search = $params['search']; + $query->where(function ($q) use ($search) { + $q->where('title', 'like', "%{$search}%") + ->orWhere('briefing_code', 'like', "%{$search}%") + ->orWhere('location', 'like', "%{$search}%") + ->orWhereHas('partner', function ($pq) use ($search) { + $pq->where('name', 'like', "%{$search}%"); + }); + }); + } + + // 상태 필터 + if (! empty($params['status']) && $params['status'] !== 'all') { + $query->where('status', $params['status']); + } + + // 입찰상태 필터 + if (! empty($params['bid_status']) && $params['bid_status'] !== 'all') { + $query->where('bid_status', $params['bid_status']); + } + + // 거래처 필터 + if (! empty($params['partner_id'])) { + $query->where('partner_id', $params['partner_id']); + } + + // 현장 필터 + if (! empty($params['site_id'])) { + $query->where('site_id', $params['site_id']); + } + + // 날짜 범위 필터 + if (! empty($params['start_date'])) { + $query->where('briefing_date', '>=', $params['start_date']); + } + if (! empty($params['end_date'])) { + $query->where('briefing_date', '<=', $params['end_date']); + } + + // 정렬 + $sortBy = $params['sort_by'] ?? 'created_at'; + $sortDir = $params['sort_dir'] ?? 'desc'; + + $sortMapping = [ + 'created_at' => 'created_at', + 'briefing_date' => 'briefing_date', + 'title' => 'title', + ]; + + $sortColumn = $sortMapping[$sortBy] ?? 'created_at'; + $query->orderBy($sortColumn, $sortDir); + + $perPage = min($params['per_page'] ?? 20, 100); + + return $query->paginate($perPage); + } + + /** + * 현장설명회 상세 조회 + */ + public function show(int $id): SiteBriefing + { + return SiteBriefing::query() + ->with(['partner:id,name', 'site:id,name,address', 'creator:id,name']) + ->where('tenant_id', $this->tenantId()) + ->findOrFail($id); + } + + /** + * 현장설명회 등록 + */ + public function store(array $data): SiteBriefing + { + $tenantId = $this->tenantId(); + $userId = $this->apiUserId(); + + return DB::transaction(function () use ($data, $tenantId, $userId) { + // 현설번호 자동 생성 + $briefingCode = SiteBriefing::generateBriefingCode($tenantId); + + // 참석자 배열 처리 + $attendees = $data['attendees'] ?? null; + $attendeeCount = is_array($attendees) ? count($attendees) : 0; + + $siteBriefing = SiteBriefing::create([ + 'tenant_id' => $tenantId, + 'briefing_code' => $briefingCode, + 'title' => $data['title'], + 'description' => $data['description'] ?? null, + 'partner_id' => $data['partner_id'] ?? null, + 'site_id' => $data['site_id'] ?? null, + 'briefing_date' => $data['briefing_date'], + 'briefing_time' => $data['briefing_time'] ?? null, + 'briefing_type' => $data['briefing_type'] ?? SiteBriefing::TYPE_OFFLINE, + 'location' => $data['location'] ?? null, + 'address' => $data['address'] ?? null, + 'status' => $data['status'] ?? SiteBriefing::STATUS_SCHEDULED, + 'bid_status' => $data['bid_status'] ?? SiteBriefing::BID_STATUS_PENDING, + 'bid_date' => $data['bid_date'] ?? null, + 'attendees' => $attendees, + 'attendee_count' => $attendeeCount, + 'attendance_status' => $data['attendance_status'] ?? SiteBriefing::ATTENDANCE_SCHEDULED, + 'site_count' => $data['site_count'] ?? 0, + 'construction_start_date' => $data['construction_start_date'] ?? null, + 'construction_end_date' => $data['construction_end_date'] ?? null, + 'vat_type' => $data['vat_type'] ?? SiteBriefing::VAT_EXCLUDED, + 'created_by' => $userId, + ]); + + return $siteBriefing->load(['partner:id,name', 'site:id,name']); + }); + } + + /** + * 현장설명회 수정 + * + * attendance_status가 'attended'(참석완료) 상태면 견적을 upsert합니다. + * - 견적이 없으면: 신규 생성 + * - 견적이 있으면: 거래처, 현장 정보 등 동기화 + */ + public function update(int $id, array $data): SiteBriefing + { + $userId = $this->apiUserId(); + + return DB::transaction(function () use ($id, $data, $userId) { + $siteBriefing = SiteBriefing::query() + ->where('tenant_id', $this->tenantId()) + ->findOrFail($id); + + // 저장 후 참석완료 상태인지 확인 + $newAttendanceStatus = $data['attendance_status'] ?? $siteBriefing->attendance_status; + $isAttended = $newAttendanceStatus === SiteBriefing::ATTENDANCE_ATTENDED; + + $updateData = array_filter([ + 'title' => $data['title'] ?? null, + 'description' => $data['description'] ?? null, + 'partner_id' => $data['partner_id'] ?? null, + 'site_id' => $data['site_id'] ?? null, + 'briefing_date' => $data['briefing_date'] ?? null, + 'briefing_time' => $data['briefing_time'] ?? null, + 'briefing_type' => $data['briefing_type'] ?? null, + 'location' => $data['location'] ?? null, + 'address' => $data['address'] ?? null, + 'status' => $data['status'] ?? null, + 'bid_status' => $data['bid_status'] ?? null, + 'bid_date' => $data['bid_date'] ?? null, + 'attendance_status' => $data['attendance_status'] ?? null, + 'site_count' => $data['site_count'] ?? null, + 'construction_start_date' => $data['construction_start_date'] ?? null, + 'construction_end_date' => $data['construction_end_date'] ?? null, + 'vat_type' => $data['vat_type'] ?? null, + ], fn ($v) => $v !== null); + + // 참석자 배열 처리 (명시적으로 전달된 경우에만 업데이트) + if (array_key_exists('attendees', $data)) { + $attendees = $data['attendees']; + $updateData['attendees'] = $attendees; + $updateData['attendee_count'] = is_array($attendees) ? count($attendees) : 0; + } + + $updateData['updated_by'] = $userId; + + $siteBriefing->update($updateData); + + // 참석완료 상태면 견적 upsert (신규 생성 또는 정보 동기화) + if ($isAttended) { + $siteBriefing->load('partner'); // partner 정보 로드 (견적에 필요) + $this->quoteService->upsertFromSiteBriefing($siteBriefing); + } + + return $siteBriefing->load(['partner:id,name', 'site:id,name']); + }); + } + + /** + * 현장설명회 삭제 + */ + public function destroy(int $id): bool + { + return DB::transaction(function () use ($id) { + $siteBriefing = SiteBriefing::query() + ->where('tenant_id', $this->tenantId()) + ->findOrFail($id); + + $siteBriefing->update(['deleted_by' => $this->apiUserId()]); + + return $siteBriefing->delete(); + }); + } + + /** + * 현장설명회 일괄 삭제 + */ + public function bulkDestroy(array $ids): int + { + return DB::transaction(function () use ($ids) { + $userId = $this->apiUserId(); + $tenantId = $this->tenantId(); + + $siteBriefings = SiteBriefing::query() + ->where('tenant_id', $tenantId) + ->whereIn('id', $ids) + ->get(); + + foreach ($siteBriefings as $siteBriefing) { + $siteBriefing->update(['deleted_by' => $userId]); + $siteBriefing->delete(); + } + + return $siteBriefings->count(); + }); + } + + /** + * 현장설명회 통계 조회 + */ + public function stats(): array + { + $tenantId = $this->tenantId(); + + $stats = SiteBriefing::query() + ->where('tenant_id', $tenantId) + ->selectRaw(" + COUNT(*) as total, + SUM(CASE WHEN status = 'scheduled' THEN 1 ELSE 0 END) as scheduled, + SUM(CASE WHEN status = 'ongoing' THEN 1 ELSE 0 END) as ongoing, + SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed, + SUM(CASE WHEN status IN ('cancelled', 'postponed') THEN 1 ELSE 0 END) as cancelled + ") + ->first(); + + return [ + 'total' => (int) $stats->total, + 'scheduled' => (int) $stats->scheduled, + 'ongoing' => (int) $stats->ongoing, + 'completed' => (int) $stats->completed, + 'cancelled' => (int) $stats->cancelled, + ]; + } +}