diff --git a/app/Http/Controllers/Api/Admin/HR/BusinessIncomeEarnerController.php b/app/Http/Controllers/Api/Admin/HR/BusinessIncomeEarnerController.php new file mode 100644 index 00000000..51e580f3 --- /dev/null +++ b/app/Http/Controllers/Api/Admin/HR/BusinessIncomeEarnerController.php @@ -0,0 +1,389 @@ +service->getBusinessIncomeEarners( + $request->all(), + $request->integer('per_page', 20) + ); + + if ($request->header('HX-Request')) { + return response(view('hr.business-income-earners.partials.table', compact('earners'))); + } + + return response()->json([ + 'success' => true, + 'data' => $earners->items(), + 'meta' => [ + 'current_page' => $earners->currentPage(), + 'last_page' => $earners->lastPage(), + 'per_page' => $earners->perPage(), + 'total' => $earners->total(), + ], + ]); + } + + /** + * 기존 사용자 검색 (사업소득자 미등록, 테넌트 소속) + */ + public function searchUsers(Request $request): JsonResponse + { + $users = $this->service->searchTenantUsers($request->get('q') ?? ''); + + return response()->json([ + 'success' => true, + 'data' => $users, + ]); + } + + /** + * 사업소득자 통계 + */ + public function stats(): JsonResponse + { + $stats = $this->service->getStats(); + + return response()->json([ + 'success' => true, + 'data' => $stats, + ]); + } + + /** + * 사업소득자 등록 + */ + public function store(Request $request): JsonResponse + { + $rules = [ + 'existing_user_id' => 'nullable|integer|exists:users,id', + 'name' => 'required|string|max:50', + 'email' => 'nullable|email|max:100', + 'phone' => 'nullable|string|max:20', + 'department_id' => 'nullable|integer|exists:departments,id', + 'position_key' => 'nullable|string|max:50', + 'job_title_key' => 'nullable|string|max:50', + 'work_location_key' => 'nullable|string|max:50', + 'employment_type_key' => 'nullable|string|max:50', + 'employee_status' => 'nullable|string|in:active,leave,resigned', + 'manager_user_id' => 'nullable|integer|exists:users,id', + 'display_name' => 'nullable|string|max:50', + 'hire_date' => 'nullable|date', + 'resign_date' => 'nullable|date', + 'address' => 'nullable|string|max:200', + 'emergency_contact' => 'nullable|string|max:100', + 'resident_number' => 'nullable|string|max:14', + 'bank_account.bank_code' => 'nullable|string|max:20', + 'bank_account.bank_name' => 'nullable|string|max:50', + 'bank_account.account_holder' => 'nullable|string|max:50', + 'bank_account.account_number' => 'nullable|string|max:30', + 'dependents' => 'nullable|array', + 'dependents.*.name' => 'required_with:dependents|string|max:50', + 'dependents.*.nationality' => 'nullable|string|in:korean,foreigner', + 'dependents.*.resident_number' => 'nullable|string|max:14', + 'dependents.*.relationship' => 'nullable|string|max:20', + 'dependents.*.is_disabled' => 'nullable|boolean', + 'dependents.*.is_dependent' => 'nullable|boolean', + // 사업장등록정보 + 'business_registration_number' => 'nullable|string|max:12', + 'business_name' => 'nullable|string|max:100', + 'business_representative' => 'nullable|string|max:50', + 'business_type' => 'nullable|string|max:50', + 'business_category' => 'nullable|string|max:50', + 'business_address' => 'nullable|string|max:200', + ]; + + if (! $request->filled('existing_user_id')) { + $rules['email'] = 'nullable|email|max:100|unique:users,email'; + } + + $validated = $request->validate($rules); + + try { + $earner = $this->service->create($validated); + + return response()->json([ + 'success' => true, + 'message' => '사업소득자가 등록되었습니다.', + 'data' => $earner, + ], 201); + } catch (\Throwable $e) { + report($e); + + return response()->json([ + 'success' => false, + 'message' => '사업소득자 등록 중 오류가 발생했습니다.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * 사업소득자 상세 조회 + */ + public function show(int $id): JsonResponse + { + $earner = $this->service->getById($id); + + if (! $earner) { + return response()->json([ + 'success' => false, + 'message' => '사업소득자 정보를 찾을 수 없습니다.', + ], 404); + } + + return response()->json([ + 'success' => true, + 'data' => $earner, + ]); + } + + /** + * 사업소득자 수정 + */ + public function update(Request $request, int $id): JsonResponse + { + $validated = $request->validate([ + 'name' => 'sometimes|required|string|max:50', + 'email' => 'nullable|email|max:100', + 'phone' => 'nullable|string|max:20', + 'department_id' => 'nullable|integer|exists:departments,id', + 'position_key' => 'nullable|string|max:50', + 'job_title_key' => 'nullable|string|max:50', + 'work_location_key' => 'nullable|string|max:50', + 'employment_type_key' => 'nullable|string|max:50', + 'employee_status' => 'nullable|string|in:active,leave,resigned', + 'manager_user_id' => 'nullable|integer|exists:users,id', + 'display_name' => 'nullable|string|max:50', + 'hire_date' => 'nullable|date', + 'resign_date' => 'nullable|date', + 'address' => 'nullable|string|max:200', + 'emergency_contact' => 'nullable|string|max:100', + 'resident_number' => 'nullable|string|max:14', + 'bank_account.bank_code' => 'nullable|string|max:20', + 'bank_account.bank_name' => 'nullable|string|max:50', + 'bank_account.account_holder' => 'nullable|string|max:50', + 'bank_account.account_number' => 'nullable|string|max:30', + 'dependents' => 'nullable|array', + 'dependents.*.name' => 'required_with:dependents|string|max:50', + 'dependents.*.nationality' => 'nullable|string|in:korean,foreigner', + 'dependents.*.resident_number' => 'nullable|string|max:14', + 'dependents.*.relationship' => 'nullable|string|max:20', + 'dependents.*.is_disabled' => 'nullable|boolean', + 'dependents.*.is_dependent' => 'nullable|boolean', + // 사업장등록정보 + 'business_registration_number' => 'nullable|string|max:12', + 'business_name' => 'nullable|string|max:100', + 'business_representative' => 'nullable|string|max:50', + 'business_type' => 'nullable|string|max:50', + 'business_category' => 'nullable|string|max:50', + 'business_address' => 'nullable|string|max:200', + ]); + + if ($request->has('dependents_submitted') && ! array_key_exists('dependents', $validated)) { + $validated['dependents'] = []; + } + + try { + $earner = $this->service->update($id, $validated); + + if (! $earner) { + return response()->json([ + 'success' => false, + 'message' => '사업소득자 정보를 찾을 수 없습니다.', + ], 404); + } + + return response()->json([ + 'success' => true, + 'message' => '사업소득자 정보가 수정되었습니다.', + 'data' => $earner, + ]); + } catch (\Throwable $e) { + report($e); + + return response()->json([ + 'success' => false, + 'message' => '사업소득자 수정 중 오류가 발생했습니다.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * 사업소득자 삭제 (퇴직 처리) + */ + public function destroy(Request $request, int $id): JsonResponse|Response + { + try { + $result = $this->service->delete($id); + + if (! $result) { + return response()->json([ + 'success' => false, + 'message' => '사업소득자 정보를 찾을 수 없습니다.', + ], 404); + } + + if ($request->header('HX-Request')) { + $earners = $this->service->getBusinessIncomeEarners($request->all(), $request->integer('per_page', 20)); + + return response(view('hr.business-income-earners.partials.table', compact('earners'))); + } + + return response()->json([ + 'success' => true, + 'message' => '퇴직 처리되었습니다.', + ]); + } catch (\Throwable $e) { + report($e); + + return response()->json([ + 'success' => false, + 'message' => '퇴직 처리 중 오류가 발생했습니다.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * 첨부파일 업로드 + */ + public function uploadFile(Request $request, int $id, GoogleCloudStorageService $gcs): JsonResponse + { + $earner = $this->service->getById($id); + if (! $earner) { + return response()->json(['success' => false, 'message' => '사업소득자 정보를 찾을 수 없습니다.'], 404); + } + + $request->validate([ + 'files' => 'required|array|max:10', + 'files.*' => 'file|max:20480', + ]); + + $tenantId = session('selected_tenant_id'); + $uploaded = []; + + foreach ($request->file('files') as $file) { + $storedName = Str::random(40).'.'.$file->getClientOriginalExtension(); + $storagePath = "business-income-earners/{$tenantId}/{$earner->id}/{$storedName}"; + + Storage::disk('tenant')->put($storagePath, file_get_contents($file)); + + $gcsUri = null; + $gcsObjectName = null; + if ($gcs->isAvailable()) { + $gcsObjectName = $storagePath; + $gcsUri = $gcs->upload($file->getRealPath(), $gcsObjectName); + } + + $fileRecord = File::create([ + 'tenant_id' => $tenantId, + 'document_id' => $earner->id, + 'document_type' => 'business_income_earner_profile', + 'original_name' => $file->getClientOriginalName(), + 'stored_name' => $storedName, + 'file_path' => $storagePath, + 'mime_type' => $file->getMimeType(), + 'file_size' => $file->getSize(), + 'file_type' => strtolower($file->getClientOriginalExtension()), + 'gcs_object_name' => $gcsObjectName, + 'gcs_uri' => $gcsUri, + 'uploaded_by' => auth()->id(), + 'created_by' => auth()->id(), + ]); + + $uploaded[] = $fileRecord; + } + + return response()->json([ + 'success' => true, + 'message' => count($uploaded).'개 파일이 업로드되었습니다.', + 'data' => $uploaded, + ], 201); + } + + /** + * 첨부파일 삭제 + */ + public function deleteFile(int $id, int $fileId, GoogleCloudStorageService $gcs): JsonResponse + { + $tenantId = session('selected_tenant_id'); + + $file = File::where('id', $fileId) + ->where('document_id', $id) + ->where('document_type', 'business_income_earner_profile') + ->where('tenant_id', $tenantId) + ->first(); + + if (! $file) { + return response()->json(['success' => false, 'message' => '파일을 찾을 수 없습니다.'], 404); + } + + if ($gcs->isAvailable() && $file->gcs_object_name) { + $gcs->delete($file->gcs_object_name); + } + + if ($file->file_path && Storage::disk('tenant')->exists($file->file_path)) { + Storage::disk('tenant')->delete($file->file_path); + } + + $file->deleted_by = auth()->id(); + $file->save(); + $file->delete(); + + return response()->json(['success' => true, 'message' => '파일이 삭제되었습니다.']); + } + + /** + * 첨부파일 다운로드 + */ + public function downloadFile(int $id, int $fileId, GoogleCloudStorageService $gcs) + { + $tenantId = session('selected_tenant_id'); + + $file = File::where('id', $fileId) + ->where('document_id', $id) + ->where('document_type', 'business_income_earner_profile') + ->where('tenant_id', $tenantId) + ->first(); + + if (! $file) { + abort(404, '파일을 찾을 수 없습니다.'); + } + + if ($gcs->isAvailable() && $file->gcs_object_name) { + $signedUrl = $gcs->getSignedUrl($file->gcs_object_name, 60); + if ($signedUrl) { + return redirect($signedUrl); + } + } + + $disk = Storage::disk('tenant'); + if ($file->file_path && $disk->exists($file->file_path)) { + return $disk->download($file->file_path, $file->original_name); + } + + abort(404, '파일이 서버에 존재하지 않습니다.'); + } +} diff --git a/app/Http/Controllers/HR/BusinessIncomeEarnerController.php b/app/Http/Controllers/HR/BusinessIncomeEarnerController.php new file mode 100644 index 00000000..a931e006 --- /dev/null +++ b/app/Http/Controllers/HR/BusinessIncomeEarnerController.php @@ -0,0 +1,88 @@ +service->getStats(); + $departments = $this->service->getDepartments(); + + return view('hr.business-income-earners.index', [ + 'stats' => $stats, + 'departments' => $departments, + ]); + } + + public function create(): View + { + $departments = $this->service->getDepartments(); + $ranks = $this->service->getPositions('rank'); + $titles = $this->service->getPositions('title'); + + return view('hr.business-income-earners.create', [ + 'departments' => $departments, + 'ranks' => $ranks, + 'titles' => $titles, + 'banks' => config('banks', []), + ]); + } + + public function show(int $id): View + { + $earner = $this->service->getById($id); + + if (! $earner) { + abort(404, '사업소득자 정보를 찾을 수 없습니다.'); + } + + $files = File::where('document_type', 'business_income_earner_profile') + ->where('document_id', $earner->id) + ->where('tenant_id', session('selected_tenant_id')) + ->orderBy('created_at', 'desc') + ->get(); + + return view('hr.business-income-earners.show', [ + 'earner' => $earner, + 'files' => $files, + ]); + } + + public function edit(int $id): View + { + $earner = $this->service->getById($id); + + if (! $earner) { + abort(404, '사업소득자 정보를 찾을 수 없습니다.'); + } + + $departments = $this->service->getDepartments(); + $ranks = $this->service->getPositions('rank'); + $titles = $this->service->getPositions('title'); + + $files = File::where('document_type', 'business_income_earner_profile') + ->where('document_id', $earner->id) + ->where('tenant_id', session('selected_tenant_id')) + ->orderBy('created_at', 'desc') + ->get(); + + return view('hr.business-income-earners.edit', [ + 'earner' => $earner, + 'departments' => $departments, + 'ranks' => $ranks, + 'titles' => $titles, + 'banks' => config('banks', []), + 'files' => $files, + ]); + } +} diff --git a/app/Models/HR/BusinessIncomeEarner.php b/app/Models/HR/BusinessIncomeEarner.php new file mode 100644 index 00000000..941e1bbf --- /dev/null +++ b/app/Models/HR/BusinessIncomeEarner.php @@ -0,0 +1,236 @@ + 'array', + 'tenant_id' => 'int', + 'user_id' => 'int', + 'department_id' => 'int', + 'manager_user_id' => 'int', + ]; + + protected $appends = [ + 'hire_date', + 'resign_date', + 'position_label', + 'job_title_label', + ]; + + // ========================================================================= + // 글로벌 스코프 + 이벤트 + // ========================================================================= + + protected static function booted(): void + { + static::addGlobalScope('business_income', function (Builder $builder) { + $builder->where('worker_type', 'business_income'); + }); + + static::creating(function (self $model) { + $model->worker_type = 'business_income'; + }); + } + + // ========================================================================= + // 관계 정의 + // ========================================================================= + + public function user(): BelongsTo + { + return $this->belongsTo(User::class, 'user_id'); + } + + public function department(): BelongsTo + { + return $this->belongsTo(Department::class, 'department_id'); + } + + public function manager(): BelongsTo + { + return $this->belongsTo(User::class, 'manager_user_id'); + } + + // ========================================================================= + // json_extra Accessor (Employee 동일) + // ========================================================================= + + public function getHireDateAttribute(): ?string + { + return $this->json_extra['hire_date'] ?? null; + } + + public function getResignDateAttribute(): ?string + { + return $this->json_extra['resign_date'] ?? null; + } + + public function getAddressAttribute(): ?string + { + return $this->json_extra['address'] ?? null; + } + + public function getEmergencyContactAttribute(): ?string + { + return $this->json_extra['emergency_contact'] ?? null; + } + + public function getResidentNumberAttribute(): ?string + { + return $this->json_extra['resident_number'] ?? null; + } + + public function getBankAccountAttribute(): ?array + { + return $this->json_extra['bank_account'] ?? null; + } + + public function getDependentsAttribute(): array + { + return $this->json_extra['dependents'] ?? []; + } + + // ========================================================================= + // 사업장등록정보 Accessor (json_extra에서 읽기) + // ========================================================================= + + public function getBusinessRegistrationNumberAttribute(): ?string + { + return $this->json_extra['business_registration_number'] ?? null; + } + + public function getBusinessNameAttribute(): ?string + { + return $this->json_extra['business_name'] ?? null; + } + + public function getBusinessRepresentativeAttribute(): ?string + { + return $this->json_extra['business_representative'] ?? null; + } + + public function getBusinessTypeAttribute(): ?string + { + return $this->json_extra['business_type'] ?? null; + } + + public function getBusinessCategoryAttribute(): ?string + { + return $this->json_extra['business_category'] ?? null; + } + + public function getBusinessAddressAttribute(): ?string + { + return $this->json_extra['business_address'] ?? null; + } + + // ========================================================================= + // 직급/직책 레이블 + // ========================================================================= + + public function getPositionLabelAttribute(): ?string + { + if (! $this->position_key || ! $this->tenant_id) { + return $this->position_key; + } + + $position = Position::where('tenant_id', $this->tenant_id) + ->where('type', Position::TYPE_RANK) + ->where('key', $this->position_key) + ->first(); + + return $position?->name ?? $this->position_key; + } + + public function getJobTitleLabelAttribute(): ?string + { + if (! $this->job_title_key || ! $this->tenant_id) { + return $this->job_title_key; + } + + $position = Position::where('tenant_id', $this->tenant_id) + ->where('type', Position::TYPE_TITLE) + ->where('key', $this->job_title_key) + ->first(); + + return $position?->name ?? $this->job_title_key; + } + + // ========================================================================= + // json_extra 헬퍼 + // ========================================================================= + + public function getJsonExtraValue(string $key, mixed $default = null): mixed + { + return $this->json_extra[$key] ?? $default; + } + + public function setJsonExtraValue(string $key, mixed $value): void + { + $extra = $this->json_extra ?? []; + if ($value === null) { + unset($extra[$key]); + } else { + $extra[$key] = $value; + } + $this->json_extra = $extra; + } + + // ========================================================================= + // 스코프 + // ========================================================================= + + public function scopeForTenant($query, ?int $tenantId = null) + { + $tenantId = $tenantId ?? session('selected_tenant_id'); + if ($tenantId) { + return $query->where('tenant_id', $tenantId); + } + + return $query; + } + + public function scopeActiveEmployees($query) + { + return $query->where('employee_status', 'active'); + } + + public function scopeOnLeave($query) + { + return $query->where('employee_status', 'leave'); + } + + public function scopeResigned($query) + { + return $query->where('employee_status', 'resigned'); + } +} diff --git a/app/Models/HR/Employee.php b/app/Models/HR/Employee.php index 1a6a8633..8437e732 100644 --- a/app/Models/HR/Employee.php +++ b/app/Models/HR/Employee.php @@ -5,6 +5,7 @@ use App\Models\Tenants\Department; use App\Models\User; use App\Traits\ModelTrait; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -23,12 +24,23 @@ class Employee extends Model 'work_location_key', 'employment_type_key', 'employee_status', + 'worker_type', 'manager_user_id', 'json_extra', 'profile_photo_path', 'display_name', ]; + protected static function booted(): void + { + static::addGlobalScope('employee', function (Builder $builder) { + $builder->where(function ($q) { + $q->where('worker_type', 'employee') + ->orWhereNull('worker_type'); + }); + }); + } + protected $casts = [ 'json_extra' => 'array', 'tenant_id' => 'int', diff --git a/app/Services/HR/BusinessIncomeEarnerService.php b/app/Services/HR/BusinessIncomeEarnerService.php new file mode 100644 index 00000000..80f8741c --- /dev/null +++ b/app/Services/HR/BusinessIncomeEarnerService.php @@ -0,0 +1,394 @@ +with(['user', 'department']) + ->forTenant($tenantId); + + if (! empty($filters['q'])) { + $search = $filters['q']; + $query->where(function ($q) use ($search) { + $q->where('display_name', 'like', "%{$search}%") + ->orWhereHas('user', function ($uq) use ($search) { + $uq->where('name', 'like', "%{$search}%") + ->orWhere('email', 'like', "%{$search}%") + ->orWhere('phone', 'like', "%{$search}%"); + }); + }); + } + + if (! empty($filters['status'])) { + $query->where('employee_status', $filters['status']); + } + + if (! empty($filters['department_id'])) { + $query->where('department_id', $filters['department_id']); + } + + $sortBy = $filters['sort_by'] ?? 'hire_date_asc'; + switch ($sortBy) { + case 'hire_date_asc': + $query->orderByRaw("JSON_UNQUOTE(JSON_EXTRACT(json_extra, '$.hire_date')) ASC"); + break; + case 'hire_date_desc': + $query->orderByRaw("JSON_UNQUOTE(JSON_EXTRACT(json_extra, '$.hire_date')) DESC"); + break; + case 'resign_date_asc': + $query->orderByRaw("JSON_UNQUOTE(JSON_EXTRACT(json_extra, '$.resign_date')) ASC"); + break; + case 'resign_date_desc': + $query->orderByRaw("JSON_UNQUOTE(JSON_EXTRACT(json_extra, '$.resign_date')) DESC"); + break; + default: + $query->orderByRaw("FIELD(employee_status, 'active', 'leave', 'resigned')") + ->orderBy('created_at', 'desc'); + break; + } + + return $query->paginate($perPage); + } + + /** + * 사업소득자 상세 조회 + */ + public function getById(int $id): ?BusinessIncomeEarner + { + $tenantId = session('selected_tenant_id'); + + return BusinessIncomeEarner::query() + ->with(['user', 'department', 'manager']) + ->forTenant($tenantId) + ->find($id); + } + + /** + * 사업소득자 통계 + */ + public function getStats(): array + { + $tenantId = session('selected_tenant_id'); + + $baseQuery = BusinessIncomeEarner::query()->forTenant($tenantId); + + return [ + 'total' => (clone $baseQuery)->count(), + 'active' => (clone $baseQuery)->where('employee_status', 'active')->count(), + 'leave' => (clone $baseQuery)->where('employee_status', 'leave')->count(), + 'resigned' => (clone $baseQuery)->where('employee_status', 'resigned')->count(), + ]; + } + + /** + * 테넌트 소속이지만 사업소득자 미등록인 사용자 검색 + */ + public function searchTenantUsers(string $query): array + { + $tenantId = session('selected_tenant_id'); + + $builder = User::query() + ->select('users.id', 'users.name', 'users.email', 'users.phone') + ->join('user_tenants as ut', function ($join) use ($tenantId) { + $join->on('users.id', '=', 'ut.user_id') + ->where('ut.tenant_id', $tenantId) + ->whereNull('ut.deleted_at'); + }) + ->leftJoin('tenant_user_profiles as tup', function ($join) use ($tenantId) { + $join->on('users.id', '=', 'tup.user_id') + ->where('tup.tenant_id', $tenantId); + }) + ->whereNull('tup.id') + ->whereNull('users.deleted_at'); + + if ($query !== '') { + $like = "%{$query}%"; + $builder->where(function ($q) use ($like) { + $q->where('users.name', 'like', $like) + ->orWhere('users.email', 'like', $like) + ->orWhere('users.phone', 'like', $like); + }); + } + + return $builder->orderBy('users.name')->limit(20)->get()->toArray(); + } + + /** + * 사업소득자 등록 (User is_active=false + TenantUserProfile 생성) + */ + public function create(array $data): BusinessIncomeEarner + { + $tenantId = session('selected_tenant_id'); + + return DB::transaction(function () use ($data, $tenantId) { + if (! empty($data['existing_user_id'])) { + $user = User::findOrFail($data['existing_user_id']); + + $isMember = $user->tenants() + ->wherePivot('tenant_id', $tenantId) + ->wherePivotNull('deleted_at') + ->exists(); + + if (! $isMember) { + throw new \RuntimeException('해당 사용자는 현재 테넌트에 소속되어 있지 않습니다.'); + } + + $alreadyRegistered = BusinessIncomeEarner::where('tenant_id', $tenantId) + ->where('user_id', $user->id) + ->exists(); + + if ($alreadyRegistered) { + throw new \RuntimeException('이미 사업소득자로 등록된 사용자입니다.'); + } + } else { + // 신규 사용자 생성 (is_active=false, 로그인 불가) + $loginId = ! empty($data['email']) + ? Str::before($data['email'], '@') + : 'BIZ_'.strtolower(Str::random(6)); + + while (User::where('user_id', $loginId)->exists()) { + $loginId = $loginId.'_'.Str::random(3); + } + + $email = ! empty($data['email']) + ? $data['email'] + : $loginId.'@placeholder.local'; + + while (User::where('email', $email)->exists()) { + $email = $loginId.'_'.Str::random(3).'@placeholder.local'; + } + + $user = User::create([ + 'user_id' => $loginId, + 'name' => $data['name'], + 'email' => $email, + 'phone' => $data['phone'] ?? null, + 'password' => Hash::make(Str::random(32)), + 'role' => 'ops', + 'is_active' => false, + 'must_change_password' => false, + 'created_by' => auth()->id(), + ]); + + if ($tenantId) { + $user->tenants()->attach($tenantId, [ + 'is_active' => true, + 'is_default' => true, + 'joined_at' => now(), + ]); + } + } + + // json_extra 구성 + $jsonExtra = []; + $scalarKeys = [ + 'hire_date', 'resign_date', 'address', 'emergency_contact', 'resident_number', + 'business_registration_number', 'business_name', 'business_representative', + 'business_type', 'business_category', 'business_address', + ]; + foreach ($scalarKeys as $key) { + if (! empty($data[$key])) { + $jsonExtra[$key] = $data[$key]; + } + } + + // 급여이체정보 + if (! empty($data['bank_account']) && is_array($data['bank_account'])) { + $bankAccount = array_filter($data['bank_account'], fn ($v) => $v !== null && $v !== ''); + if (! empty($bankAccount)) { + $jsonExtra['bank_account'] = $bankAccount; + } + } + + // 부양가족 정보 + if (! empty($data['dependents']) && is_array($data['dependents'])) { + $dependents = array_values(array_filter($data['dependents'], function ($dep) { + return ! empty($dep['name']); + })); + $dependents = array_map(function ($dep) { + $dep['is_disabled'] = filter_var($dep['is_disabled'] ?? false, FILTER_VALIDATE_BOOLEAN); + $dep['is_dependent'] = filter_var($dep['is_dependent'] ?? false, FILTER_VALIDATE_BOOLEAN); + + return $dep; + }, $dependents); + if (! empty($dependents)) { + $jsonExtra['dependents'] = $dependents; + } + } + + $earner = BusinessIncomeEarner::create([ + 'tenant_id' => $tenantId, + 'user_id' => $user->id, + 'department_id' => $data['department_id'] ?? null, + 'position_key' => $data['position_key'] ?? null, + 'job_title_key' => $data['job_title_key'] ?? null, + 'work_location_key' => $data['work_location_key'] ?? null, + 'employment_type_key' => $data['employment_type_key'] ?? null, + 'employee_status' => $data['employee_status'] ?? 'active', + 'manager_user_id' => $data['manager_user_id'] ?? null, + 'display_name' => $data['display_name'] ?? $data['name'], + 'json_extra' => ! empty($jsonExtra) ? $jsonExtra : null, + ]); + + return $earner->load(['user', 'department']); + }); + } + + /** + * 사업소득자 정보 수정 + */ + public function update(int $id, array $data): ?BusinessIncomeEarner + { + $earner = $this->getById($id); + if (! $earner) { + return null; + } + + $updateData = array_filter([ + 'department_id' => $data['department_id'] ?? null, + 'position_key' => $data['position_key'] ?? null, + 'job_title_key' => $data['job_title_key'] ?? null, + 'work_location_key' => $data['work_location_key'] ?? null, + 'employment_type_key' => $data['employment_type_key'] ?? null, + 'employee_status' => $data['employee_status'] ?? null, + 'manager_user_id' => $data['manager_user_id'] ?? null, + 'display_name' => $data['display_name'] ?? null, + ], fn ($v) => $v !== null); + + // json_extra 업데이트 + $jsonExtraKeys = [ + 'hire_date', 'resign_date', 'address', 'emergency_contact', 'salary', 'resident_number', + 'business_registration_number', 'business_name', 'business_representative', + 'business_type', 'business_category', 'business_address', + ]; + $extra = $earner->json_extra ?? []; + foreach ($jsonExtraKeys as $key) { + if (array_key_exists($key, $data)) { + if ($data[$key] === null || $data[$key] === '') { + unset($extra[$key]); + } else { + $extra[$key] = $data[$key]; + } + } + } + + // 급여이체정보 + if (array_key_exists('bank_account', $data)) { + if (! empty($data['bank_account']) && is_array($data['bank_account'])) { + $bankAccount = array_filter($data['bank_account'], fn ($v) => $v !== null && $v !== ''); + if (! empty($bankAccount)) { + $extra['bank_account'] = $bankAccount; + } else { + unset($extra['bank_account']); + } + } else { + unset($extra['bank_account']); + } + } + + // 부양가족 정보 + if (array_key_exists('dependents', $data)) { + if (! empty($data['dependents']) && is_array($data['dependents'])) { + $dependents = array_values(array_filter($data['dependents'], function ($dep) { + return ! empty($dep['name']); + })); + $dependents = array_map(function ($dep) { + $dep['is_disabled'] = filter_var($dep['is_disabled'] ?? false, FILTER_VALIDATE_BOOLEAN); + $dep['is_dependent'] = filter_var($dep['is_dependent'] ?? false, FILTER_VALIDATE_BOOLEAN); + + return $dep; + }, $dependents); + if (! empty($dependents)) { + $extra['dependents'] = $dependents; + } else { + unset($extra['dependents']); + } + } else { + unset($extra['dependents']); + } + } + + $updateData['json_extra'] = ! empty($extra) ? $extra : null; + + $earner->update($updateData); + + // User 기본정보 동기화 + if ($earner->user) { + $userUpdate = []; + if (! empty($data['name'])) { + $userUpdate['name'] = $data['name']; + } + if (! empty($data['email'])) { + $userUpdate['email'] = $data['email']; + } + if (! empty($data['phone'])) { + $userUpdate['phone'] = $data['phone']; + } + if (! empty($userUpdate)) { + $earner->user->update($userUpdate); + } + } + + return $earner->fresh(['user', 'department']); + } + + /** + * 사업소득자 삭제 (퇴직 처리) + */ + public function delete(int $id): bool + { + $earner = $this->getById($id); + if (! $earner) { + return false; + } + + $earner->update(['employee_status' => 'resigned']); + + 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 getPositions(string $type = 'rank'): \Illuminate\Database\Eloquent\Collection + { + return Position::query() + ->forTenant() + ->where('type', $type) + ->where('is_active', true) + ->ordered() + ->get(['id', 'key', 'name']); + } +} diff --git a/resources/views/hr/business-income-earners/create.blade.php b/resources/views/hr/business-income-earners/create.blade.php new file mode 100644 index 00000000..77dab886 --- /dev/null +++ b/resources/views/hr/business-income-earners/create.blade.php @@ -0,0 +1,533 @@ +@extends('layouts.app') + +@section('title', '사업소득자 등록') + +@section('content') +
등록된 파일이 없습니다.
+ @endforelse +파일을 드래그하거나 클릭하여 업로드
+파일당 최대 20MB
+{{ now()->format('Y년 n월 j일') }} 현재
+| 사업소득자 | +부서 | +직급/직책 | +상태 | ++ + | +연락처 | +작업 | +
|---|---|---|---|---|---|---|
|
+
+
+ {{ mb_substr($earner->display_name ?? $earner->user?->name ?? '?', 0, 1) }}
+
+
+
+
+
+ {{ $earner->display_name ?? $earner->user?->name ?? '-' }}
+
+ |
+
+ {{-- 부서 --}}
+ + {{ $earner->department?->name ?? '-' }} + | + + {{-- 직급/직책 --}} +
+ {{ $earner->position_label ?? '-' }}
+ @if($earner->job_title_label)
+ {{ $earner->job_title_label }}
+ @endif
+ |
+
+ {{-- 상태 --}}
+ + @switch($earner->employee_status) + @case('active') + + 계약중 + + @break + @case('leave') + + 휴직 + + @break + @case('resigned') + + 계약종료 + + @break + @default + + {{ $earner->employee_status ?? '-' }} + + @endswitch + | + + {{-- 계약일 --}} ++ {{ $earner->hire_date ?? '-' }} + | + + {{-- 연락처 --}} ++ {{ $earner->user?->phone ?? $earner->user?->email ?? '-' }} + | + + {{-- 작업 --}} ++ + | +
|
+
+
+
+ 등록된 사업소득자가 없습니다. + + 첫 번째 사업소득자 등록하기 → + + |
+ ||||||
| 이름 | +내/외국인 | +주민등록번호 | +관계 | +장애인 | +피부양자 | +
|---|---|---|---|---|---|
| {{ $dep['name'] ?? '-' }} | +{{ ($dep['nationality'] ?? 'korean') === 'korean' ? '내국인' : '외국인' }} | ++ @if(!empty($dep['resident_number'])) + {{ Str::mask($dep['resident_number'], '*', 8) }} + @else + - + @endif + | ++ @switch($dep['relationship'] ?? '') + @case('spouse') 배우자 @break + @case('child') 자녀 @break + @case('parent') 부모 @break + @case('sibling') 형제자매 @break + @case('grandparent') 조부모 @break + @case('other') 기타 @break + @default - @break + @endswitch + | +{{ filter_var($dep['is_disabled'] ?? false, FILTER_VALIDATE_BOOLEAN) ? 'Y' : 'N' }} | +{{ filter_var($dep['is_dependent'] ?? false, FILTER_VALIDATE_BOOLEAN) ? 'Y' : 'N' }} | +
등록된 부양가족이 없습니다.
+등록된 파일이 없습니다.
+