- 차원 테이블: 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>
88 lines
3.5 KiB
PHP
88 lines
3.5 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Stats;
|
|
|
|
use App\Models\Stats\Daily\StatHrAttendanceDaily;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class HrStatService implements StatDomainServiceInterface
|
|
{
|
|
public function aggregateDaily(int $tenantId, Carbon $date): int
|
|
{
|
|
$dateStr = $date->format('Y-m-d');
|
|
|
|
// 전체 직원 수 (tenant_user_profiles 기준)
|
|
$totalEmployees = DB::connection('mysql')
|
|
->table('user_tenants')
|
|
->where('tenant_id', $tenantId)
|
|
->where('is_active', true)
|
|
->count();
|
|
|
|
// 근태 (attendances)
|
|
$attendanceStats = DB::connection('mysql')
|
|
->table('attendances')
|
|
->where('tenant_id', $tenantId)
|
|
->where('base_date', $dateStr)
|
|
->whereNull('deleted_at')
|
|
->selectRaw("
|
|
COUNT(*) as total_count,
|
|
SUM(CASE WHEN status = 'onTime' THEN 1 ELSE 0 END) as on_time_count,
|
|
SUM(CASE WHEN status = 'late' THEN 1 ELSE 0 END) as late_count,
|
|
SUM(CASE WHEN status = 'absent' THEN 1 ELSE 0 END) as absent_count,
|
|
SUM(CASE WHEN status = 'overtime' THEN 1 ELSE 0 END) as overtime_count
|
|
")
|
|
->first();
|
|
|
|
$attendanceCount = ($attendanceStats->on_time_count ?? 0)
|
|
+ ($attendanceStats->late_count ?? 0)
|
|
+ ($attendanceStats->overtime_count ?? 0);
|
|
$attendanceRate = $totalEmployees > 0 ? ($attendanceCount / $totalEmployees) * 100 : 0;
|
|
|
|
// 휴가 (leaves)
|
|
$leaveStats = DB::connection('mysql')
|
|
->table('leaves')
|
|
->where('tenant_id', $tenantId)
|
|
->where('start_date', '<=', $dateStr)
|
|
->where('end_date', '>=', $dateStr)
|
|
->where('status', 'approved')
|
|
->whereNull('deleted_at')
|
|
->selectRaw("
|
|
COUNT(*) as total_count,
|
|
SUM(CASE WHEN leave_type = 'annual' THEN 1 ELSE 0 END) as annual_count,
|
|
SUM(CASE WHEN leave_type = 'sick' THEN 1 ELSE 0 END) as sick_count,
|
|
SUM(CASE WHEN leave_type NOT IN ('annual', 'sick') THEN 1 ELSE 0 END) as other_count
|
|
")
|
|
->first();
|
|
|
|
// 초과근무 (attendances status = 'overtime')
|
|
$overtimeCount = $attendanceStats->overtime_count ?? 0;
|
|
|
|
StatHrAttendanceDaily::updateOrCreate(
|
|
['tenant_id' => $tenantId, 'stat_date' => $dateStr],
|
|
[
|
|
'total_employees' => $totalEmployees,
|
|
'attendance_count' => $attendanceCount,
|
|
'late_count' => $attendanceStats->late_count ?? 0,
|
|
'absent_count' => $attendanceStats->absent_count ?? 0,
|
|
'attendance_rate' => $attendanceRate,
|
|
'leave_count' => $leaveStats->total_count ?? 0,
|
|
'leave_annual_count' => $leaveStats->annual_count ?? 0,
|
|
'leave_sick_count' => $leaveStats->sick_count ?? 0,
|
|
'leave_other_count' => $leaveStats->other_count ?? 0,
|
|
'overtime_hours' => 0, // attendances에 시간 정보 없음
|
|
'overtime_employee_count' => $overtimeCount,
|
|
'total_labor_cost' => 0, // 일간 인건비는 급여 정산 시 계산
|
|
]
|
|
);
|
|
|
|
return 1;
|
|
}
|
|
|
|
public function aggregateMonthly(int $tenantId, int $year, int $month): int
|
|
{
|
|
// 인사 도메인은 일간 테이블만 운영 (월간은 Phase 4에서 필요시 추가)
|
|
return 0;
|
|
}
|
|
}
|