feat: sam_stat P1 도메인 확장 (Phase 3)
- 차원 테이블: dim_client, dim_product 마이그레이션 + SCD Type 2 동기화 (DimensionSyncService) - 재고 통계: stat_inventory_daily + InventoryStatService (stocks, stock_transactions, inspections) - 견적/영업 통계: stat_quote_pipeline_daily + QuoteStatService (quotes, biddings, sales_prospects) - 인사/근태 통계: stat_hr_attendance_daily + HrStatService (attendances, leaves, user_tenants) - KPI/알림: stat_kpi_targets, stat_alerts + KpiAlertService + StatCheckKpiAlertsCommand - StatAggregatorService에 inventory, quote, hr 도메인 추가 (총 6개 도메인) - 스케줄러: stat:check-kpi-alerts 매일 09:00 등록 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
93
app/Services/Stats/QuoteStatService.php
Normal file
93
app/Services/Stats/QuoteStatService.php
Normal file
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Stats;
|
||||
|
||||
use App\Models\Stats\Daily\StatQuotePipelineDaily;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class QuoteStatService implements StatDomainServiceInterface
|
||||
{
|
||||
public function aggregateDaily(int $tenantId, Carbon $date): int
|
||||
{
|
||||
$dateStr = $date->format('Y-m-d');
|
||||
|
||||
// 견적 (quotes)
|
||||
$quoteStats = DB::connection('mysql')
|
||||
->table('quotes')
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('registration_date', $dateStr)
|
||||
->whereNull('deleted_at')
|
||||
->selectRaw("
|
||||
COUNT(*) as created_count,
|
||||
COALESCE(SUM(total_amount), 0) as total_amount,
|
||||
SUM(CASE WHEN status = 'approved' THEN 1 ELSE 0 END) as approved_count,
|
||||
SUM(CASE WHEN status = 'rejected' THEN 1 ELSE 0 END) as rejected_count,
|
||||
SUM(CASE WHEN status = 'converted' THEN 1 ELSE 0 END) as conversion_count
|
||||
")
|
||||
->first();
|
||||
|
||||
$createdCount = $quoteStats->created_count ?? 0;
|
||||
$conversionCount = $quoteStats->conversion_count ?? 0;
|
||||
$conversionRate = $createdCount > 0 ? ($conversionCount / $createdCount) * 100 : 0;
|
||||
|
||||
// 입찰 (biddings)
|
||||
$biddingStats = DB::connection('mysql')
|
||||
->table('biddings')
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('bidding_date', $dateStr)
|
||||
->whereNull('deleted_at')
|
||||
->selectRaw("
|
||||
COUNT(*) as cnt,
|
||||
SUM(CASE WHEN status = 'won' THEN 1 ELSE 0 END) as won_count,
|
||||
COALESCE(SUM(bidding_amount), 0) as total_amount
|
||||
")
|
||||
->first();
|
||||
|
||||
// 상담 (sales_prospect_consultations)
|
||||
$consultationCount = DB::connection('mysql')
|
||||
->table('sales_prospect_consultations')
|
||||
->whereDate('created_at', $dateStr)
|
||||
->count();
|
||||
|
||||
// 영업 기회 (sales_prospects - tenant_id 없음, created_at 기반)
|
||||
$prospectStats = DB::connection('mysql')
|
||||
->table('sales_prospects')
|
||||
->whereDate('created_at', $dateStr)
|
||||
->whereNull('deleted_at')
|
||||
->selectRaw("
|
||||
COUNT(*) as created_count,
|
||||
SUM(CASE WHEN status = 'contracted' THEN 1 ELSE 0 END) as won_count,
|
||||
SUM(CASE WHEN status = 'lost' THEN 1 ELSE 0 END) as lost_count
|
||||
")
|
||||
->first();
|
||||
|
||||
StatQuotePipelineDaily::updateOrCreate(
|
||||
['tenant_id' => $tenantId, 'stat_date' => $dateStr],
|
||||
[
|
||||
'quote_created_count' => $createdCount,
|
||||
'quote_amount' => $quoteStats->total_amount ?? 0,
|
||||
'quote_approved_count' => $quoteStats->approved_count ?? 0,
|
||||
'quote_rejected_count' => $quoteStats->rejected_count ?? 0,
|
||||
'quote_conversion_count' => $conversionCount,
|
||||
'quote_conversion_rate' => $conversionRate,
|
||||
'prospect_created_count' => $prospectStats->created_count ?? 0,
|
||||
'prospect_won_count' => $prospectStats->won_count ?? 0,
|
||||
'prospect_lost_count' => $prospectStats->lost_count ?? 0,
|
||||
'prospect_amount' => 0, // sales_prospects에 금액 컬럼 없음
|
||||
'bidding_count' => $biddingStats->cnt ?? 0,
|
||||
'bidding_won_count' => $biddingStats->won_count ?? 0,
|
||||
'bidding_amount' => $biddingStats->total_amount ?? 0,
|
||||
'consultation_count' => $consultationCount,
|
||||
]
|
||||
);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
public function aggregateMonthly(int $tenantId, int $year, int $month): int
|
||||
{
|
||||
// 견적 도메인은 일간 테이블만 운영 (월간은 Phase 4에서 필요시 추가)
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user