- Attendance 모델 (attendances 테이블, 상태/색상 매핑, check_in/check_out accessor) - AttendanceService (목록/월간통계/CRUD, 부서/사원 드롭다운) - API 컨트롤러 (HTMX+JSON 이중 응답, stats/index/store/update/destroy) - 페이지 컨트롤러 (index 페이지 렌더링) - 웹/API 라우트 등록 (hr/attendances, api/admin/hr/attendances) - index.blade.php (통계카드+필터+등록/수정 모달) - partials/table.blade.php (HTMX 부분 로드 테이블)
227 lines
7.0 KiB
PHP
227 lines
7.0 KiB
PHP
<?php
|
|
|
|
namespace App\Services\HR;
|
|
|
|
use App\Models\HR\Attendance;
|
|
use App\Models\HR\Employee;
|
|
use App\Models\Tenants\Department;
|
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class AttendanceService
|
|
{
|
|
/**
|
|
* 근태 목록 조회 (페이지네이션)
|
|
*/
|
|
public function getAttendances(array $filters = [], int $perPage = 20): LengthAwarePaginator
|
|
{
|
|
$tenantId = session('selected_tenant_id');
|
|
|
|
$query = Attendance::query()
|
|
->with(['user', 'user.tenantProfiles' => fn ($q) => $q->where('tenant_id', $tenantId)])
|
|
->forTenant($tenantId);
|
|
|
|
// 이름 검색
|
|
if (! empty($filters['q'])) {
|
|
$search = $filters['q'];
|
|
$query->whereHas('user', function ($q) use ($search) {
|
|
$q->where('name', 'like', "%{$search}%");
|
|
});
|
|
}
|
|
|
|
// 부서 필터
|
|
if (! empty($filters['department_id'])) {
|
|
$deptId = $filters['department_id'];
|
|
$query->whereHas('user.tenantProfiles', function ($q) use ($tenantId, $deptId) {
|
|
$q->where('tenant_id', $tenantId)->where('department_id', $deptId);
|
|
});
|
|
}
|
|
|
|
// 상태 필터
|
|
if (! empty($filters['status'])) {
|
|
$query->where('status', $filters['status']);
|
|
}
|
|
|
|
// 날짜 범위 필터
|
|
if (! empty($filters['date_from']) && ! empty($filters['date_to'])) {
|
|
$query->betweenDates($filters['date_from'], $filters['date_to']);
|
|
} elseif (! empty($filters['date_from'])) {
|
|
$query->whereDate('base_date', '>=', $filters['date_from']);
|
|
} elseif (! empty($filters['date_to'])) {
|
|
$query->whereDate('base_date', '<=', $filters['date_to']);
|
|
}
|
|
|
|
$query->orderBy('base_date', 'desc')
|
|
->orderBy('created_at', 'desc');
|
|
|
|
return $query->paginate($perPage);
|
|
}
|
|
|
|
/**
|
|
* 월간 통계 (상태별 카운트)
|
|
*/
|
|
public function getMonthlyStats(?int $year = null, ?int $month = null): array
|
|
{
|
|
$tenantId = session('selected_tenant_id');
|
|
$year = $year ?? now()->year;
|
|
$month = $month ?? now()->month;
|
|
|
|
$startDate = sprintf('%04d-%02d-01', $year, $month);
|
|
$endDate = now()->year == $year && now()->month == $month
|
|
? now()->toDateString()
|
|
: sprintf('%04d-%02d-%02d', $year, $month, cal_days_in_month(CAL_GREGORIAN, $month, $year));
|
|
|
|
$counts = Attendance::query()
|
|
->forTenant($tenantId)
|
|
->betweenDates($startDate, $endDate)
|
|
->select('status', DB::raw('COUNT(*) as cnt'))
|
|
->groupBy('status')
|
|
->pluck('cnt', 'status')
|
|
->toArray();
|
|
|
|
return [
|
|
'onTime' => $counts['onTime'] ?? 0,
|
|
'late' => $counts['late'] ?? 0,
|
|
'absent' => $counts['absent'] ?? 0,
|
|
'vacation' => $counts['vacation'] ?? 0,
|
|
'etc' => ($counts['businessTrip'] ?? 0) + ($counts['fieldWork'] ?? 0) + ($counts['overtime'] ?? 0) + ($counts['remote'] ?? 0),
|
|
'year' => $year,
|
|
'month' => $month,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 근태 등록 (Upsert: tenant_id + user_id + base_date)
|
|
*/
|
|
public function storeAttendance(array $data): Attendance
|
|
{
|
|
$tenantId = session('selected_tenant_id');
|
|
|
|
return DB::transaction(function () use ($data, $tenantId) {
|
|
$jsonDetails = [];
|
|
if (! empty($data['check_in'])) {
|
|
$jsonDetails['check_in'] = $data['check_in'];
|
|
}
|
|
if (! empty($data['check_out'])) {
|
|
$jsonDetails['check_out'] = $data['check_out'];
|
|
}
|
|
|
|
$attendance = Attendance::updateOrCreate(
|
|
[
|
|
'tenant_id' => $tenantId,
|
|
'user_id' => $data['user_id'],
|
|
'base_date' => $data['base_date'],
|
|
],
|
|
[
|
|
'status' => $data['status'] ?? 'onTime',
|
|
'json_details' => ! empty($jsonDetails) ? $jsonDetails : null,
|
|
'remarks' => $data['remarks'] ?? null,
|
|
'created_by' => auth()->id(),
|
|
'updated_by' => auth()->id(),
|
|
]
|
|
);
|
|
|
|
return $attendance->load('user');
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 근태 수정
|
|
*/
|
|
public function updateAttendance(int $id, array $data): ?Attendance
|
|
{
|
|
$tenantId = session('selected_tenant_id');
|
|
|
|
$attendance = Attendance::query()
|
|
->forTenant($tenantId)
|
|
->find($id);
|
|
|
|
if (! $attendance) {
|
|
return null;
|
|
}
|
|
|
|
$updateData = [];
|
|
|
|
if (array_key_exists('status', $data)) {
|
|
$updateData['status'] = $data['status'];
|
|
}
|
|
if (array_key_exists('remarks', $data)) {
|
|
$updateData['remarks'] = $data['remarks'];
|
|
}
|
|
|
|
// json_details 업데이트
|
|
$jsonDetails = $attendance->json_details ?? [];
|
|
if (array_key_exists('check_in', $data)) {
|
|
if ($data['check_in']) {
|
|
$jsonDetails['check_in'] = $data['check_in'];
|
|
} else {
|
|
unset($jsonDetails['check_in']);
|
|
}
|
|
}
|
|
if (array_key_exists('check_out', $data)) {
|
|
if ($data['check_out']) {
|
|
$jsonDetails['check_out'] = $data['check_out'];
|
|
} else {
|
|
unset($jsonDetails['check_out']);
|
|
}
|
|
}
|
|
$updateData['json_details'] = ! empty($jsonDetails) ? $jsonDetails : null;
|
|
$updateData['updated_by'] = auth()->id();
|
|
|
|
$attendance->update($updateData);
|
|
|
|
return $attendance->fresh('user');
|
|
}
|
|
|
|
/**
|
|
* 근태 삭제
|
|
*/
|
|
public function deleteAttendance(int $id): bool
|
|
{
|
|
$tenantId = session('selected_tenant_id');
|
|
|
|
$attendance = Attendance::query()
|
|
->forTenant($tenantId)
|
|
->find($id);
|
|
|
|
if (! $attendance) {
|
|
return false;
|
|
}
|
|
|
|
$attendance->update(['deleted_by' => auth()->id()]);
|
|
$attendance->delete();
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* 부서 목록 (드롭다운용)
|
|
*/
|
|
public function getDepartments(): \Illuminate\Database\Eloquent\Collection
|
|
{
|
|
$tenantId = session('selected_tenant_id');
|
|
|
|
return Department::query()
|
|
->where('is_active', true)
|
|
->when($tenantId, fn ($q) => $q->where('tenant_id', $tenantId))
|
|
->orderBy('sort_order')
|
|
->orderBy('name')
|
|
->get(['id', 'name', 'code']);
|
|
}
|
|
|
|
/**
|
|
* 활성 사원 목록 (드롭다운용)
|
|
*/
|
|
public function getActiveEmployees(): \Illuminate\Database\Eloquent\Collection
|
|
{
|
|
$tenantId = session('selected_tenant_id');
|
|
|
|
return Employee::query()
|
|
->with('user:id,name')
|
|
->forTenant($tenantId)
|
|
->activeEmployees()
|
|
->orderBy('display_name')
|
|
->get(['id', 'user_id', 'display_name', 'department_id']);
|
|
}
|
|
}
|