Phase 0 - 공통 모듈: - ExportService.php 생성 (Maatwebsite/Excel 기반 엑셀 내보내기) - BulkUpdateAccountCodeRequest.php 생성 (계정과목 일괄변경 유효성 검사) Phase 1 - 계정과목 일괄변경: - WithdrawalController/Service: bulkUpdateAccountCode 메서드 추가 - DepositController/Service: bulkUpdateAccountCode 메서드 추가 - POST /v1/withdrawals/bulk-update-account-code - POST /v1/deposits/bulk-update-account-code Phase 2 - 엑셀 내보내기: - AttendanceController/Service: export, getExportData 메서드 추가 - SalaryController/Service: export, getExportData 메서드 추가 - GET /v1/attendances/export - GET /v1/salaries/export Co-Authored-By: Claude <noreply@anthropic.com>
401 lines
13 KiB
PHP
401 lines
13 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\Tenants\Salary;
|
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class SalaryService extends Service
|
|
{
|
|
/**
|
|
* 급여 목록 조회
|
|
*/
|
|
public function index(array $params): LengthAwarePaginator
|
|
{
|
|
$tenantId = $this->tenantId();
|
|
|
|
$query = Salary::query()
|
|
->where('tenant_id', $tenantId)
|
|
->with([
|
|
'employee:id,name,user_id,email',
|
|
'employeeProfile' => fn ($q) => $q->where('tenant_id', $tenantId),
|
|
'employeeProfile.department:id,name',
|
|
]);
|
|
|
|
// 검색 필터 (직원명)
|
|
if (! empty($params['search'])) {
|
|
$search = $params['search'];
|
|
$query->whereHas('employee', function ($q) use ($search) {
|
|
$q->where('name', 'like', "%{$search}%");
|
|
});
|
|
}
|
|
|
|
// 연도 필터
|
|
if (! empty($params['year'])) {
|
|
$query->where('year', $params['year']);
|
|
}
|
|
|
|
// 월 필터
|
|
if (! empty($params['month'])) {
|
|
$query->where('month', $params['month']);
|
|
}
|
|
|
|
// 상태 필터
|
|
if (! empty($params['status'])) {
|
|
$query->where('status', $params['status']);
|
|
}
|
|
|
|
// 기간 필터
|
|
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'])) {
|
|
$query->where('employee_id', $params['employee_id']);
|
|
}
|
|
|
|
// 정렬
|
|
$sortBy = $params['sort_by'] ?? 'year';
|
|
$sortDir = $params['sort_dir'] ?? 'desc';
|
|
|
|
if ($sortBy === 'year') {
|
|
$query->orderBy('year', $sortDir)
|
|
->orderBy('month', $sortDir);
|
|
} else {
|
|
$query->orderBy($sortBy, $sortDir);
|
|
}
|
|
|
|
// 페이지네이션
|
|
$perPage = $params['per_page'] ?? 20;
|
|
|
|
return $query->paginate($perPage);
|
|
}
|
|
|
|
/**
|
|
* 급여 상세 조회
|
|
*/
|
|
public function show(int $id): Salary
|
|
{
|
|
$tenantId = $this->tenantId();
|
|
|
|
return Salary::query()
|
|
->where('tenant_id', $tenantId)
|
|
->with([
|
|
'employee:id,name,user_id,email',
|
|
'employeeProfile' => fn ($q) => $q->where('tenant_id', $tenantId),
|
|
'employeeProfile.department:id,name',
|
|
])
|
|
->findOrFail($id);
|
|
}
|
|
|
|
/**
|
|
* 급여 등록
|
|
*/
|
|
public function store(array $data): Salary
|
|
{
|
|
$tenantId = $this->tenantId();
|
|
$userId = $this->apiUserId();
|
|
|
|
return DB::transaction(function () use ($data, $tenantId, $userId) {
|
|
$salary = new Salary;
|
|
$salary->tenant_id = $tenantId;
|
|
$salary->employee_id = $data['employee_id'];
|
|
$salary->year = $data['year'];
|
|
$salary->month = $data['month'];
|
|
$salary->base_salary = $data['base_salary'] ?? 0;
|
|
$salary->total_allowance = $data['total_allowance'] ?? 0;
|
|
$salary->total_overtime = $data['total_overtime'] ?? 0;
|
|
$salary->total_bonus = $data['total_bonus'] ?? 0;
|
|
$salary->total_deduction = $data['total_deduction'] ?? 0;
|
|
$salary->allowance_details = $data['allowance_details'] ?? null;
|
|
$salary->deduction_details = $data['deduction_details'] ?? null;
|
|
$salary->payment_date = $data['payment_date'] ?? null;
|
|
$salary->status = $data['status'] ?? 'scheduled';
|
|
$salary->created_by = $userId;
|
|
$salary->updated_by = $userId;
|
|
|
|
// 실지급액 계산
|
|
$salary->net_payment = $salary->calculateNetPayment();
|
|
|
|
$salary->save();
|
|
|
|
return $salary->load('employee:id,name,email');
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 급여 수정
|
|
*/
|
|
public function update(int $id, array $data): Salary
|
|
{
|
|
$tenantId = $this->tenantId();
|
|
$userId = $this->apiUserId();
|
|
|
|
return DB::transaction(function () use ($id, $data, $tenantId, $userId) {
|
|
$salary = Salary::query()
|
|
->where('tenant_id', $tenantId)
|
|
->findOrFail($id);
|
|
|
|
if (isset($data['employee_id'])) {
|
|
$salary->employee_id = $data['employee_id'];
|
|
}
|
|
if (isset($data['year'])) {
|
|
$salary->year = $data['year'];
|
|
}
|
|
if (isset($data['month'])) {
|
|
$salary->month = $data['month'];
|
|
}
|
|
if (isset($data['base_salary'])) {
|
|
$salary->base_salary = $data['base_salary'];
|
|
}
|
|
if (isset($data['total_allowance'])) {
|
|
$salary->total_allowance = $data['total_allowance'];
|
|
}
|
|
if (isset($data['total_overtime'])) {
|
|
$salary->total_overtime = $data['total_overtime'];
|
|
}
|
|
if (isset($data['total_bonus'])) {
|
|
$salary->total_bonus = $data['total_bonus'];
|
|
}
|
|
if (isset($data['total_deduction'])) {
|
|
$salary->total_deduction = $data['total_deduction'];
|
|
}
|
|
if (array_key_exists('allowance_details', $data)) {
|
|
$salary->allowance_details = $data['allowance_details'];
|
|
}
|
|
if (array_key_exists('deduction_details', $data)) {
|
|
$salary->deduction_details = $data['deduction_details'];
|
|
}
|
|
if (array_key_exists('payment_date', $data)) {
|
|
$salary->payment_date = $data['payment_date'];
|
|
}
|
|
if (isset($data['status'])) {
|
|
$salary->status = $data['status'];
|
|
}
|
|
|
|
// 실지급액 재계산
|
|
$salary->net_payment = $salary->calculateNetPayment();
|
|
|
|
$salary->updated_by = $userId;
|
|
$salary->save();
|
|
|
|
return $salary->fresh()->load([
|
|
'employee:id,name,user_id,email',
|
|
'employeeProfile' => fn ($q) => $q->where('tenant_id', $tenantId),
|
|
'employeeProfile.department:id,name',
|
|
]);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 급여 삭제
|
|
*/
|
|
public function destroy(int $id): bool
|
|
{
|
|
$tenantId = $this->tenantId();
|
|
$userId = $this->apiUserId();
|
|
|
|
return DB::transaction(function () use ($id, $tenantId, $userId) {
|
|
$salary = Salary::query()
|
|
->where('tenant_id', $tenantId)
|
|
->findOrFail($id);
|
|
|
|
$salary->deleted_by = $userId;
|
|
$salary->save();
|
|
$salary->delete();
|
|
|
|
return true;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 급여 상태 변경 (지급완료/지급예정)
|
|
*/
|
|
public function updateStatus(int $id, string $status): Salary
|
|
{
|
|
$tenantId = $this->tenantId();
|
|
$userId = $this->apiUserId();
|
|
|
|
return DB::transaction(function () use ($id, $status, $tenantId, $userId) {
|
|
$salary = Salary::query()
|
|
->where('tenant_id', $tenantId)
|
|
->findOrFail($id);
|
|
|
|
$salary->status = $status;
|
|
$salary->updated_by = $userId;
|
|
$salary->save();
|
|
|
|
return $salary->load([
|
|
'employee:id,name,user_id,email',
|
|
'employeeProfile' => fn ($q) => $q->where('tenant_id', $tenantId),
|
|
'employeeProfile.department:id,name',
|
|
]);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 급여 일괄 상태 변경
|
|
*/
|
|
public function bulkUpdateStatus(array $ids, string $status): int
|
|
{
|
|
$tenantId = $this->tenantId();
|
|
$userId = $this->apiUserId();
|
|
|
|
return DB::transaction(function () use ($ids, $status, $tenantId, $userId) {
|
|
return Salary::query()
|
|
->where('tenant_id', $tenantId)
|
|
->whereIn('id', $ids)
|
|
->update([
|
|
'status' => $status,
|
|
'updated_by' => $userId,
|
|
'updated_at' => now(),
|
|
]);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 엑셀 내보내기용 데이터 조회
|
|
*
|
|
* @return array{data: array<int, array<string, mixed>>, headings: array<int, string>}
|
|
*/
|
|
public function getExportData(array $params): array
|
|
{
|
|
$tenantId = $this->tenantId();
|
|
|
|
$query = Salary::query()
|
|
->where('tenant_id', $tenantId)
|
|
->with([
|
|
'employee:id,name,user_id,email',
|
|
'employeeProfile' => fn ($q) => $q->where('tenant_id', $tenantId),
|
|
'employeeProfile.department:id,name',
|
|
]);
|
|
|
|
// 검색 필터 (직원명)
|
|
if (! empty($params['search'])) {
|
|
$search = $params['search'];
|
|
$query->whereHas('employee', function ($q) use ($search) {
|
|
$q->where('name', 'like', "%{$search}%");
|
|
});
|
|
}
|
|
|
|
// 연도 필터
|
|
if (! empty($params['year'])) {
|
|
$query->where('year', $params['year']);
|
|
}
|
|
|
|
// 월 필터
|
|
if (! empty($params['month'])) {
|
|
$query->where('month', $params['month']);
|
|
}
|
|
|
|
// 상태 필터
|
|
if (! empty($params['status'])) {
|
|
$query->where('status', $params['status']);
|
|
}
|
|
|
|
// 기간 필터
|
|
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'])) {
|
|
$query->where('employee_id', $params['employee_id']);
|
|
}
|
|
|
|
// 정렬
|
|
$sortBy = $params['sort_by'] ?? 'year';
|
|
$sortDir = $params['sort_dir'] ?? 'desc';
|
|
|
|
if ($sortBy === 'year') {
|
|
$query->orderBy('year', $sortDir)
|
|
->orderBy('month', $sortDir);
|
|
} else {
|
|
$query->orderBy($sortBy, $sortDir);
|
|
}
|
|
|
|
$salaries = $query->get();
|
|
|
|
// 상태 레이블 매핑
|
|
$statusLabels = [
|
|
'scheduled' => '지급예정',
|
|
'completed' => '지급완료',
|
|
'pending' => '보류',
|
|
];
|
|
|
|
// 엑셀 데이터 변환
|
|
$data = $salaries->map(function ($salary) use ($statusLabels) {
|
|
return [
|
|
$salary->year.'년 '.$salary->month.'월',
|
|
$salary->employee?->name ?? '-',
|
|
$salary->employeeProfile?->department?->name ?? '-',
|
|
number_format($salary->base_salary),
|
|
number_format($salary->total_allowance),
|
|
number_format($salary->total_overtime),
|
|
number_format($salary->total_bonus),
|
|
number_format($salary->total_deduction),
|
|
number_format($salary->net_payment),
|
|
$statusLabels[$salary->status] ?? $salary->status,
|
|
$salary->payment_date ?? '-',
|
|
];
|
|
})->toArray();
|
|
|
|
$headings = [
|
|
'급여월',
|
|
'직원명',
|
|
'부서',
|
|
'기본급',
|
|
'수당',
|
|
'야근수당',
|
|
'상여금',
|
|
'공제액',
|
|
'실지급액',
|
|
'상태',
|
|
'지급일',
|
|
];
|
|
|
|
return [
|
|
'data' => $data,
|
|
'headings' => $headings,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 급여 통계 조회
|
|
*/
|
|
public function getStatistics(array $params): array
|
|
{
|
|
$tenantId = $this->tenantId();
|
|
|
|
$query = Salary::query()
|
|
->where('tenant_id', $tenantId);
|
|
|
|
// 연도/월 필터
|
|
if (! empty($params['year'])) {
|
|
$query->where('year', $params['year']);
|
|
}
|
|
if (! empty($params['month'])) {
|
|
$query->where('month', $params['month']);
|
|
}
|
|
|
|
// 기간 필터
|
|
if (! empty($params['start_date']) && ! empty($params['end_date'])) {
|
|
$query->whereBetween('payment_date', [$params['start_date'], $params['end_date']]);
|
|
}
|
|
|
|
return [
|
|
'total_net_payment' => (float) $query->sum('net_payment'),
|
|
'total_base_salary' => (float) $query->sum('base_salary'),
|
|
'total_allowance' => (float) $query->sum('total_allowance'),
|
|
'total_overtime' => (float) $query->sum('total_overtime'),
|
|
'total_bonus' => (float) $query->sum('total_bonus'),
|
|
'total_deduction' => (float) $query->sum('total_deduction'),
|
|
'count' => $query->count(),
|
|
'completed_count' => (clone $query)->where('status', 'completed')->count(),
|
|
'scheduled_count' => (clone $query)->where('status', 'scheduled')->count(),
|
|
];
|
|
}
|
|
}
|