feat: [hr] 인사관리 사원관리 Phase 1 구현
- Employee, Position 모델 생성 (tenant_user_profiles, positions 테이블) - EmployeeService 생성 (CRUD, 통계, 필터/검색/페이지네이션) - 뷰 컨트롤러(HR/EmployeeController) + API 컨트롤러 생성 - Blade 뷰: index(통계카드+HTMX테이블), create, edit, show, partials/table - 라우트: web.php(/hr/employees/*), api.php(/admin/hr/employees/*)
This commit is contained in:
173
app/Http/Controllers/Api/Admin/HR/EmployeeController.php
Normal file
173
app/Http/Controllers/Api/Admin/HR/EmployeeController.php
Normal file
@@ -0,0 +1,173 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin\HR;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\HR\EmployeeService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
class EmployeeController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private EmployeeService $employeeService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 사원 목록 조회 (HTMX → HTML / 일반 → JSON)
|
||||
*/
|
||||
public function index(Request $request): JsonResponse|Response
|
||||
{
|
||||
$employees = $this->employeeService->getEmployees(
|
||||
$request->all(),
|
||||
$request->integer('per_page', 20)
|
||||
);
|
||||
|
||||
if ($request->header('HX-Request')) {
|
||||
return response(view('hr.employees.partials.table', compact('employees')));
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $employees->items(),
|
||||
'meta' => [
|
||||
'current_page' => $employees->currentPage(),
|
||||
'last_page' => $employees->lastPage(),
|
||||
'per_page' => $employees->perPage(),
|
||||
'total' => $employees->total(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 사원 통계
|
||||
*/
|
||||
public function stats(): JsonResponse
|
||||
{
|
||||
$stats = $this->employeeService->getStats();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $stats,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 사원 등록
|
||||
*/
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:50',
|
||||
'email' => 'nullable|email|max:100|unique:users,email',
|
||||
'phone' => 'nullable|string|max:20',
|
||||
'password' => 'nullable|string|min:6',
|
||||
'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',
|
||||
'employee_code' => 'nullable|string|max:30',
|
||||
'hire_date' => 'nullable|date',
|
||||
'address' => 'nullable|string|max:200',
|
||||
'emergency_contact' => 'nullable|string|max:100',
|
||||
]);
|
||||
|
||||
$employee = $this->employeeService->createEmployee($validated);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '사원이 등록되었습니다.',
|
||||
'data' => $employee,
|
||||
], 201);
|
||||
}
|
||||
|
||||
/**
|
||||
* 사원 상세 조회
|
||||
*/
|
||||
public function show(int $id): JsonResponse
|
||||
{
|
||||
$employee = $this->employeeService->getEmployeeById($id);
|
||||
|
||||
if (! $employee) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '사원 정보를 찾을 수 없습니다.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $employee,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 사원 수정
|
||||
*/
|
||||
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',
|
||||
'employee_code' => 'nullable|string|max:30',
|
||||
'hire_date' => 'nullable|date',
|
||||
'address' => 'nullable|string|max:200',
|
||||
'emergency_contact' => 'nullable|string|max:100',
|
||||
]);
|
||||
|
||||
$employee = $this->employeeService->updateEmployee($id, $validated);
|
||||
|
||||
if (! $employee) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '사원 정보를 찾을 수 없습니다.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '사원 정보가 수정되었습니다.',
|
||||
'data' => $employee,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 사원 삭제 (퇴직 처리)
|
||||
*/
|
||||
public function destroy(Request $request, int $id): JsonResponse|Response
|
||||
{
|
||||
$result = $this->employeeService->deleteEmployee($id);
|
||||
|
||||
if (! $result) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '사원 정보를 찾을 수 없습니다.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
if ($request->header('HX-Request')) {
|
||||
$employees = $this->employeeService->getEmployees($request->all(), $request->integer('per_page', 20));
|
||||
|
||||
return response(view('hr.employees.partials.table', compact('employees')));
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '퇴직 처리되었습니다.',
|
||||
]);
|
||||
}
|
||||
}
|
||||
83
app/Http/Controllers/HR/EmployeeController.php
Normal file
83
app/Http/Controllers/HR/EmployeeController.php
Normal file
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\HR;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\HR\EmployeeService;
|
||||
use Illuminate\Contracts\View\View;
|
||||
|
||||
class EmployeeController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private EmployeeService $employeeService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 사원 목록 페이지
|
||||
*/
|
||||
public function index(): View
|
||||
{
|
||||
$stats = $this->employeeService->getStats();
|
||||
$departments = $this->employeeService->getDepartments();
|
||||
|
||||
return view('hr.employees.index', [
|
||||
'stats' => $stats,
|
||||
'departments' => $departments,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 사원 등록 폼
|
||||
*/
|
||||
public function create(): View
|
||||
{
|
||||
$departments = $this->employeeService->getDepartments();
|
||||
$ranks = $this->employeeService->getPositions('rank');
|
||||
$titles = $this->employeeService->getPositions('title');
|
||||
|
||||
return view('hr.employees.create', [
|
||||
'departments' => $departments,
|
||||
'ranks' => $ranks,
|
||||
'titles' => $titles,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 사원 상세 페이지
|
||||
*/
|
||||
public function show(int $id): View
|
||||
{
|
||||
$employee = $this->employeeService->getEmployeeById($id);
|
||||
|
||||
if (! $employee) {
|
||||
abort(404, '사원 정보를 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
return view('hr.employees.show', [
|
||||
'employee' => $employee,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 사원 수정 폼
|
||||
*/
|
||||
public function edit(int $id): View
|
||||
{
|
||||
$employee = $this->employeeService->getEmployeeById($id);
|
||||
|
||||
if (! $employee) {
|
||||
abort(404, '사원 정보를 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
$departments = $this->employeeService->getDepartments();
|
||||
$ranks = $this->employeeService->getPositions('rank');
|
||||
$titles = $this->employeeService->getPositions('title');
|
||||
|
||||
return view('hr.employees.edit', [
|
||||
'employee' => $employee,
|
||||
'departments' => $departments,
|
||||
'ranks' => $ranks,
|
||||
'titles' => $titles,
|
||||
]);
|
||||
}
|
||||
}
|
||||
166
app/Models/HR/Employee.php
Normal file
166
app/Models/HR/Employee.php
Normal file
@@ -0,0 +1,166 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\HR;
|
||||
|
||||
use App\Models\Tenants\Department;
|
||||
use App\Models\User;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Employee extends Model
|
||||
{
|
||||
use ModelTrait;
|
||||
|
||||
protected $table = 'tenant_user_profiles';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'user_id',
|
||||
'department_id',
|
||||
'position_key',
|
||||
'job_title_key',
|
||||
'work_location_key',
|
||||
'employment_type_key',
|
||||
'employee_status',
|
||||
'manager_user_id',
|
||||
'json_extra',
|
||||
'profile_photo_path',
|
||||
'display_name',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'json_extra' => 'array',
|
||||
'tenant_id' => 'int',
|
||||
'user_id' => 'int',
|
||||
'department_id' => 'int',
|
||||
'manager_user_id' => 'int',
|
||||
];
|
||||
|
||||
protected $appends = [
|
||||
'employee_code',
|
||||
'hire_date',
|
||||
'position_label',
|
||||
'job_title_label',
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
// 관계 정의
|
||||
// =========================================================================
|
||||
|
||||
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
|
||||
// =========================================================================
|
||||
|
||||
public function getEmployeeCodeAttribute(): ?string
|
||||
{
|
||||
return $this->json_extra['employee_code'] ?? null;
|
||||
}
|
||||
|
||||
public function getHireDateAttribute(): ?string
|
||||
{
|
||||
return $this->json_extra['hire_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 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');
|
||||
}
|
||||
}
|
||||
61
app/Models/HR/Position.php
Normal file
61
app/Models/HR/Position.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\HR;
|
||||
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Position extends Model
|
||||
{
|
||||
use ModelTrait;
|
||||
|
||||
protected $table = 'positions';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'type',
|
||||
'key',
|
||||
'name',
|
||||
'sort_order',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'tenant_id' => 'int',
|
||||
'sort_order' => 'int',
|
||||
'is_active' => 'bool',
|
||||
];
|
||||
|
||||
public const TYPE_RANK = 'rank';
|
||||
|
||||
public const TYPE_TITLE = 'title';
|
||||
|
||||
// =========================================================================
|
||||
// 스코프
|
||||
// =========================================================================
|
||||
|
||||
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 scopeRanks($query)
|
||||
{
|
||||
return $query->where('type', self::TYPE_RANK);
|
||||
}
|
||||
|
||||
public function scopeTitles($query)
|
||||
{
|
||||
return $query->where('type', self::TYPE_TITLE);
|
||||
}
|
||||
|
||||
public function scopeOrdered($query)
|
||||
{
|
||||
return $query->orderBy('sort_order');
|
||||
}
|
||||
}
|
||||
237
app/Services/HR/EmployeeService.php
Normal file
237
app/Services/HR/EmployeeService.php
Normal file
@@ -0,0 +1,237 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\HR;
|
||||
|
||||
use App\Models\HR\Employee;
|
||||
use App\Models\HR\Position;
|
||||
use App\Models\Tenants\Department;
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class EmployeeService
|
||||
{
|
||||
/**
|
||||
* 사원 목록 조회 (페이지네이션)
|
||||
*/
|
||||
public function getEmployees(array $filters = [], int $perPage = 20): LengthAwarePaginator
|
||||
{
|
||||
$tenantId = session('selected_tenant_id');
|
||||
|
||||
$query = Employee::query()
|
||||
->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}%");
|
||||
})
|
||||
->orWhereRaw("JSON_UNQUOTE(JSON_EXTRACT(json_extra, '$.employee_code')) LIKE ?", ["%{$search}%"]);
|
||||
});
|
||||
}
|
||||
|
||||
// 상태 필터
|
||||
if (! empty($filters['status'])) {
|
||||
$query->where('employee_status', $filters['status']);
|
||||
}
|
||||
|
||||
// 부서 필터
|
||||
if (! empty($filters['department_id'])) {
|
||||
$query->where('department_id', $filters['department_id']);
|
||||
}
|
||||
|
||||
// 정렬
|
||||
$query->orderByRaw("FIELD(employee_status, 'active', 'leave', 'resigned')")
|
||||
->orderBy('created_at', 'desc');
|
||||
|
||||
return $query->paginate($perPage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 사원 상세 조회
|
||||
*/
|
||||
public function getEmployeeById(int $id): ?Employee
|
||||
{
|
||||
$tenantId = session('selected_tenant_id');
|
||||
|
||||
return Employee::query()
|
||||
->with(['user', 'department', 'manager'])
|
||||
->forTenant($tenantId)
|
||||
->find($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 사원 통계
|
||||
*/
|
||||
public function getStats(): array
|
||||
{
|
||||
$tenantId = session('selected_tenant_id');
|
||||
|
||||
$baseQuery = Employee::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(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 사원 등록 (User + TenantUserProfile 동시 생성)
|
||||
*/
|
||||
public function createEmployee(array $data): Employee
|
||||
{
|
||||
$tenantId = session('selected_tenant_id');
|
||||
|
||||
return DB::transaction(function () use ($data, $tenantId) {
|
||||
// User 생성
|
||||
$user = User::create([
|
||||
'name' => $data['name'],
|
||||
'email' => $data['email'] ?? null,
|
||||
'phone' => $data['phone'] ?? null,
|
||||
'password' => bcrypt($data['password'] ?? 'sam1234!'),
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
// json_extra 구성
|
||||
$jsonExtra = [];
|
||||
if (! empty($data['employee_code'])) {
|
||||
$jsonExtra['employee_code'] = $data['employee_code'];
|
||||
}
|
||||
if (! empty($data['hire_date'])) {
|
||||
$jsonExtra['hire_date'] = $data['hire_date'];
|
||||
}
|
||||
if (! empty($data['address'])) {
|
||||
$jsonExtra['address'] = $data['address'];
|
||||
}
|
||||
if (! empty($data['emergency_contact'])) {
|
||||
$jsonExtra['emergency_contact'] = $data['emergency_contact'];
|
||||
}
|
||||
|
||||
// Employee(TenantUserProfile) 생성
|
||||
$employee = Employee::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 $employee->load(['user', 'department']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 사원 정보 수정
|
||||
*/
|
||||
public function updateEmployee(int $id, array $data): ?Employee
|
||||
{
|
||||
$employee = $this->getEmployeeById($id);
|
||||
if (! $employee) {
|
||||
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 = ['employee_code', 'hire_date', 'address', 'emergency_contact', 'salary', 'bank_account'];
|
||||
$extra = $employee->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];
|
||||
}
|
||||
}
|
||||
}
|
||||
$updateData['json_extra'] = ! empty($extra) ? $extra : null;
|
||||
|
||||
$employee->update($updateData);
|
||||
|
||||
// User 기본정보 동기화
|
||||
if ($employee->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)) {
|
||||
$employee->user->update($userUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
return $employee->fresh(['user', 'department']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 사원 삭제 (퇴직 처리)
|
||||
*/
|
||||
public function deleteEmployee(int $id): bool
|
||||
{
|
||||
$employee = $this->getEmployeeById($id);
|
||||
if (! $employee) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$employee->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']);
|
||||
}
|
||||
}
|
||||
185
resources/views/hr/employees/create.blade.php
Normal file
185
resources/views/hr/employees/create.blade.php
Normal file
@@ -0,0 +1,185 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', '사원 등록')
|
||||
|
||||
@section('content')
|
||||
<div class="container mx-auto px-4 py-6 max-w-2xl">
|
||||
{{-- 페이지 헤더 --}}
|
||||
<div class="mb-6">
|
||||
<a href="{{ route('hr.employees.index') }}" class="text-sm text-gray-500 hover:text-gray-700 inline-flex items-center gap-1 mb-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
||||
</svg>
|
||||
사원 목록으로
|
||||
</a>
|
||||
<h1 class="text-2xl font-bold text-gray-800">사원 등록</h1>
|
||||
</div>
|
||||
|
||||
{{-- 등록 폼 --}}
|
||||
<div class="bg-white rounded-lg shadow-sm p-6">
|
||||
<form id="employeeForm"
|
||||
hx-post="{{ route('api.admin.hr.employees.store') }}"
|
||||
hx-headers='{"X-CSRF-TOKEN": "{{ csrf_token() }}", "Accept": "application/json"}'
|
||||
hx-target="#form-message"
|
||||
hx-swap="innerHTML"
|
||||
class="space-y-6">
|
||||
|
||||
<div id="form-message"></div>
|
||||
|
||||
{{-- 기본 정보 --}}
|
||||
<div class="border-b border-gray-200 pb-4 mb-4">
|
||||
<h2 class="text-lg font-semibold text-gray-700">기본 정보</h2>
|
||||
</div>
|
||||
|
||||
{{-- 이름 --}}
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
이름 <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input type="text" name="name" id="name" required
|
||||
placeholder="홍길동"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
|
||||
{{-- 사번 --}}
|
||||
<div>
|
||||
<label for="employee_code" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
사번
|
||||
</label>
|
||||
<input type="text" name="employee_code" id="employee_code"
|
||||
placeholder="EMP-001"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
|
||||
{{-- 이메일 / 연락처 --}}
|
||||
<div class="flex gap-4" style="flex-wrap: wrap;">
|
||||
<div style="flex: 1 1 200px;">
|
||||
<label for="email" class="block text-sm font-medium text-gray-700 mb-1">이메일</label>
|
||||
<input type="email" name="email" id="email"
|
||||
placeholder="user@example.com"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
<div style="flex: 1 1 200px;">
|
||||
<label for="phone" class="block text-sm font-medium text-gray-700 mb-1">연락처</label>
|
||||
<input type="text" name="phone" id="phone"
|
||||
placeholder="010-1234-5678"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 비밀번호 --}}
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
비밀번호
|
||||
</label>
|
||||
<input type="password" name="password" id="password"
|
||||
placeholder="미입력 시 기본 비밀번호(sam1234!) 설정"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
<p class="text-xs text-gray-400 mt-1">미입력 시 기본 비밀번호가 설정됩니다.</p>
|
||||
</div>
|
||||
|
||||
{{-- 근무 정보 --}}
|
||||
<div class="border-b border-gray-200 pb-4 mb-4 mt-8">
|
||||
<h2 class="text-lg font-semibold text-gray-700">근무 정보</h2>
|
||||
</div>
|
||||
|
||||
{{-- 부서 --}}
|
||||
<div>
|
||||
<label for="department_id" class="block text-sm font-medium text-gray-700 mb-1">부서</label>
|
||||
<select name="department_id" id="department_id"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
<option value="">선택하세요</option>
|
||||
@foreach($departments as $dept)
|
||||
<option value="{{ $dept->id }}">{{ $dept->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{{-- 직급 / 직책 --}}
|
||||
<div class="flex gap-4" style="flex-wrap: wrap;">
|
||||
<div style="flex: 1 1 200px;">
|
||||
<label for="position_key" class="block text-sm font-medium text-gray-700 mb-1">직급</label>
|
||||
<select name="position_key" id="position_key"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
<option value="">선택하세요</option>
|
||||
@foreach($ranks as $rank)
|
||||
<option value="{{ $rank->key }}">{{ $rank->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div style="flex: 1 1 200px;">
|
||||
<label for="job_title_key" class="block text-sm font-medium text-gray-700 mb-1">직책</label>
|
||||
<select name="job_title_key" id="job_title_key"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
<option value="">선택하세요</option>
|
||||
@foreach($titles as $title)
|
||||
<option value="{{ $title->key }}">{{ $title->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 입사일 / 상태 --}}
|
||||
<div class="flex gap-4" style="flex-wrap: wrap;">
|
||||
<div style="flex: 1 1 200px;">
|
||||
<label for="hire_date" class="block text-sm font-medium text-gray-700 mb-1">입사일</label>
|
||||
<input type="date" name="hire_date" id="hire_date"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
<div style="flex: 1 1 200px;">
|
||||
<label for="employee_status" class="block text-sm font-medium text-gray-700 mb-1">재직상태</label>
|
||||
<select name="employee_status" id="employee_status"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
<option value="active" selected>재직</option>
|
||||
<option value="leave">휴직</option>
|
||||
<option value="resigned">퇴직</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 주소 --}}
|
||||
<div>
|
||||
<label for="address" class="block text-sm font-medium text-gray-700 mb-1">주소</label>
|
||||
<input type="text" name="address" id="address"
|
||||
placeholder="주소를 입력하세요"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
|
||||
{{-- 비상연락처 --}}
|
||||
<div>
|
||||
<label for="emergency_contact" class="block text-sm font-medium text-gray-700 mb-1">비상연락처</label>
|
||||
<input type="text" name="emergency_contact" id="emergency_contact"
|
||||
placeholder="긴급 시 연락할 전화번호"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
|
||||
{{-- 버튼 --}}
|
||||
<div class="flex justify-end gap-3 pt-4 border-t">
|
||||
<a href="{{ route('hr.employees.index') }}"
|
||||
class="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors">
|
||||
취소
|
||||
</a>
|
||||
<button type="submit"
|
||||
class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors">
|
||||
등록
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
document.body.addEventListener('htmx:afterRequest', function(event) {
|
||||
if (event.detail.successful) {
|
||||
try {
|
||||
const response = JSON.parse(event.detail.xhr.responseText);
|
||||
if (response.success) {
|
||||
window.location.href = '{{ route('hr.employees.index') }}';
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
188
resources/views/hr/employees/edit.blade.php
Normal file
188
resources/views/hr/employees/edit.blade.php
Normal file
@@ -0,0 +1,188 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', '사원 수정')
|
||||
|
||||
@section('content')
|
||||
<div class="container mx-auto px-4 py-6 max-w-2xl">
|
||||
{{-- 페이지 헤더 --}}
|
||||
<div class="mb-6">
|
||||
<a href="{{ route('hr.employees.index') }}" class="text-sm text-gray-500 hover:text-gray-700 inline-flex items-center gap-1 mb-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
||||
</svg>
|
||||
사원 목록으로
|
||||
</a>
|
||||
<h1 class="text-2xl font-bold text-gray-800">사원 수정</h1>
|
||||
</div>
|
||||
|
||||
{{-- 수정 폼 --}}
|
||||
<div class="bg-white rounded-lg shadow-sm p-6">
|
||||
<form id="employeeForm"
|
||||
hx-put="{{ route('api.admin.hr.employees.update', $employee->id) }}"
|
||||
hx-headers='{"X-CSRF-TOKEN": "{{ csrf_token() }}", "Accept": "application/json"}'
|
||||
hx-target="#form-message"
|
||||
hx-swap="innerHTML"
|
||||
class="space-y-6">
|
||||
|
||||
<div id="form-message"></div>
|
||||
|
||||
{{-- 기본 정보 --}}
|
||||
<div class="border-b border-gray-200 pb-4 mb-4">
|
||||
<h2 class="text-lg font-semibold text-gray-700">기본 정보</h2>
|
||||
</div>
|
||||
|
||||
{{-- 이름 --}}
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
이름 <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input type="text" name="name" id="name" required
|
||||
value="{{ $employee->user?->name }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
|
||||
{{-- 표시 이름 --}}
|
||||
<div>
|
||||
<label for="display_name" class="block text-sm font-medium text-gray-700 mb-1">표시 이름</label>
|
||||
<input type="text" name="display_name" id="display_name"
|
||||
value="{{ $employee->display_name }}"
|
||||
placeholder="사이드바 등에 표시되는 이름"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
|
||||
{{-- 사번 --}}
|
||||
<div>
|
||||
<label for="employee_code" class="block text-sm font-medium text-gray-700 mb-1">사번</label>
|
||||
<input type="text" name="employee_code" id="employee_code"
|
||||
value="{{ $employee->employee_code }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
|
||||
{{-- 이메일 / 연락처 --}}
|
||||
<div class="flex gap-4" style="flex-wrap: wrap;">
|
||||
<div style="flex: 1 1 200px;">
|
||||
<label for="email" class="block text-sm font-medium text-gray-700 mb-1">이메일</label>
|
||||
<input type="email" name="email" id="email"
|
||||
value="{{ $employee->user?->email }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
<div style="flex: 1 1 200px;">
|
||||
<label for="phone" class="block text-sm font-medium text-gray-700 mb-1">연락처</label>
|
||||
<input type="text" name="phone" id="phone"
|
||||
value="{{ $employee->user?->phone }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 근무 정보 --}}
|
||||
<div class="border-b border-gray-200 pb-4 mb-4 mt-8">
|
||||
<h2 class="text-lg font-semibold text-gray-700">근무 정보</h2>
|
||||
</div>
|
||||
|
||||
{{-- 부서 --}}
|
||||
<div>
|
||||
<label for="department_id" class="block text-sm font-medium text-gray-700 mb-1">부서</label>
|
||||
<select name="department_id" id="department_id"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
<option value="">선택하세요</option>
|
||||
@foreach($departments as $dept)
|
||||
<option value="{{ $dept->id }}" {{ $employee->department_id == $dept->id ? 'selected' : '' }}>
|
||||
{{ $dept->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{{-- 직급 / 직책 --}}
|
||||
<div class="flex gap-4" style="flex-wrap: wrap;">
|
||||
<div style="flex: 1 1 200px;">
|
||||
<label for="position_key" class="block text-sm font-medium text-gray-700 mb-1">직급</label>
|
||||
<select name="position_key" id="position_key"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
<option value="">선택하세요</option>
|
||||
@foreach($ranks as $rank)
|
||||
<option value="{{ $rank->key }}" {{ $employee->position_key === $rank->key ? 'selected' : '' }}>
|
||||
{{ $rank->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div style="flex: 1 1 200px;">
|
||||
<label for="job_title_key" class="block text-sm font-medium text-gray-700 mb-1">직책</label>
|
||||
<select name="job_title_key" id="job_title_key"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
<option value="">선택하세요</option>
|
||||
@foreach($titles as $title)
|
||||
<option value="{{ $title->key }}" {{ $employee->job_title_key === $title->key ? 'selected' : '' }}>
|
||||
{{ $title->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 입사일 / 상태 --}}
|
||||
<div class="flex gap-4" style="flex-wrap: wrap;">
|
||||
<div style="flex: 1 1 200px;">
|
||||
<label for="hire_date" class="block text-sm font-medium text-gray-700 mb-1">입사일</label>
|
||||
<input type="date" name="hire_date" id="hire_date"
|
||||
value="{{ $employee->hire_date }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
<div style="flex: 1 1 200px;">
|
||||
<label for="employee_status" class="block text-sm font-medium text-gray-700 mb-1">재직상태</label>
|
||||
<select name="employee_status" id="employee_status"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
<option value="active" {{ $employee->employee_status === 'active' ? 'selected' : '' }}>재직</option>
|
||||
<option value="leave" {{ $employee->employee_status === 'leave' ? 'selected' : '' }}>휴직</option>
|
||||
<option value="resigned" {{ $employee->employee_status === 'resigned' ? 'selected' : '' }}>퇴직</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 주소 --}}
|
||||
<div>
|
||||
<label for="address" class="block text-sm font-medium text-gray-700 mb-1">주소</label>
|
||||
<input type="text" name="address" id="address"
|
||||
value="{{ $employee->address }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
|
||||
{{-- 비상연락처 --}}
|
||||
<div>
|
||||
<label for="emergency_contact" class="block text-sm font-medium text-gray-700 mb-1">비상연락처</label>
|
||||
<input type="text" name="emergency_contact" id="emergency_contact"
|
||||
value="{{ $employee->emergency_contact }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
|
||||
{{-- 버튼 --}}
|
||||
<div class="flex justify-end gap-3 pt-4 border-t">
|
||||
<a href="{{ route('hr.employees.show', $employee->id) }}"
|
||||
class="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors">
|
||||
취소
|
||||
</a>
|
||||
<button type="submit"
|
||||
class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors">
|
||||
저장
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
document.body.addEventListener('htmx:afterRequest', function(event) {
|
||||
if (event.detail.successful) {
|
||||
try {
|
||||
const response = JSON.parse(event.detail.xhr.responseText);
|
||||
if (response.success) {
|
||||
window.location.href = '{{ route('hr.employees.show', $employee->id) }}';
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
112
resources/views/hr/employees/index.blade.php
Normal file
112
resources/views/hr/employees/index.blade.php
Normal file
@@ -0,0 +1,112 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', '사원관리')
|
||||
|
||||
@section('content')
|
||||
<div class="px-4 py-6">
|
||||
{{-- 페이지 헤더 --}}
|
||||
<div class="flex flex-col lg:flex-row lg:justify-between lg:items-center gap-4 mb-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-800">사원관리</h1>
|
||||
<p class="text-sm text-gray-500 mt-1">{{ now()->format('Y년 n월 j일') }} 현재</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2 sm:gap-3">
|
||||
<a href="{{ route('hr.employees.create') }}"
|
||||
class="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
|
||||
</svg>
|
||||
사원 등록
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 통계 카드 --}}
|
||||
<div class="grid gap-4 mb-6" style="grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));">
|
||||
<div class="bg-white rounded-lg shadow-sm p-4">
|
||||
<div class="text-sm text-gray-500">전체</div>
|
||||
<div class="text-2xl font-bold text-gray-800">{{ $stats['total'] }}명</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg shadow-sm p-4">
|
||||
<div class="text-sm text-gray-500">재직</div>
|
||||
<div class="text-2xl font-bold text-emerald-600">{{ $stats['active'] }}명</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg shadow-sm p-4">
|
||||
<div class="text-sm text-gray-500">휴직</div>
|
||||
<div class="text-2xl font-bold text-amber-600">{{ $stats['leave'] }}명</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg shadow-sm p-4">
|
||||
<div class="text-sm text-gray-500">퇴직</div>
|
||||
<div class="text-2xl font-bold text-red-600">{{ $stats['resigned'] }}명</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 테이블 컨테이너 --}}
|
||||
<div class="bg-white rounded-lg shadow-sm overflow-hidden">
|
||||
{{-- 필터 --}}
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<x-filter-collapsible id="employeeFilter">
|
||||
<form id="employeeFilterForm" class="flex flex-wrap gap-3 items-end">
|
||||
<div style="flex: 1 1 200px; max-width: 300px;">
|
||||
<label class="block text-xs text-gray-500 mb-1">검색</label>
|
||||
<input type="text" name="q" placeholder="이름, 사번, 이메일..."
|
||||
value="{{ request('q') }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
<div style="flex: 0 1 160px;">
|
||||
<label class="block text-xs text-gray-500 mb-1">부서</label>
|
||||
<select name="department_id"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
<option value="">전체 부서</option>
|
||||
@foreach($departments as $dept)
|
||||
<option value="{{ $dept->id }}" {{ request('department_id') == $dept->id ? 'selected' : '' }}>
|
||||
{{ $dept->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div style="flex: 0 1 130px;">
|
||||
<label class="block text-xs text-gray-500 mb-1">상태</label>
|
||||
<select name="status"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
<option value="">전체 상태</option>
|
||||
<option value="active" {{ request('status') === 'active' ? 'selected' : '' }}>재직</option>
|
||||
<option value="leave" {{ request('status') === 'leave' ? 'selected' : '' }}>휴직</option>
|
||||
<option value="resigned" {{ request('status') === 'resigned' ? 'selected' : '' }}>퇴직</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="shrink-0">
|
||||
<button type="submit"
|
||||
hx-get="{{ route('api.admin.hr.employees.index') }}"
|
||||
hx-target="#employees-table"
|
||||
hx-include="#employeeFilterForm"
|
||||
class="px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white text-sm rounded-lg transition-colors">
|
||||
검색
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</x-filter-collapsible>
|
||||
</div>
|
||||
|
||||
{{-- HTMX 테이블 영역 --}}
|
||||
<div id="employees-table"
|
||||
hx-get="{{ route('api.admin.hr.employees.index') }}"
|
||||
hx-trigger="load"
|
||||
hx-headers='{"X-CSRF-TOKEN": "{{ csrf_token() }}"}'
|
||||
class="min-h-[200px]">
|
||||
<div class="flex justify-center items-center p-12">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
document.getElementById('employeeFilterForm')?.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
htmx.trigger('#employees-table', 'htmx:trigger');
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
146
resources/views/hr/employees/partials/table.blade.php
Normal file
146
resources/views/hr/employees/partials/table.blade.php
Normal file
@@ -0,0 +1,146 @@
|
||||
{{-- 사원 목록 테이블 (HTMX로 로드) --}}
|
||||
<x-table-swipe>
|
||||
<table class="min-w-full">
|
||||
<thead class="bg-gray-50 border-b border-gray-200">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-sm font-semibold text-gray-600">사원</th>
|
||||
<th class="px-6 py-3 text-left text-sm font-semibold text-gray-600">부서</th>
|
||||
<th class="px-6 py-3 text-left text-sm font-semibold text-gray-600">직급/직책</th>
|
||||
<th class="px-6 py-3 text-center text-sm font-semibold text-gray-600">상태</th>
|
||||
<th class="px-6 py-3 text-center text-sm font-semibold text-gray-600">입사일</th>
|
||||
<th class="px-6 py-3 text-center text-sm font-semibold text-gray-600">연락처</th>
|
||||
<th class="px-6 py-3 text-center text-sm font-semibold text-gray-600">작업</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-100">
|
||||
@forelse($employees as $employee)
|
||||
<tr class="hover:bg-gray-50 transition-colors {{ $employee->employee_status === 'resigned' ? 'opacity-50' : '' }}">
|
||||
{{-- 사원 정보 --}}
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<a href="{{ route('hr.employees.show', $employee->id) }}"
|
||||
class="flex items-center gap-3 group">
|
||||
<div class="shrink-0 w-9 h-9 rounded-full bg-blue-100 text-blue-600 flex items-center justify-center text-sm font-medium">
|
||||
{{ mb_substr($employee->display_name ?? $employee->user?->name ?? '?', 0, 1) }}
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm font-medium text-gray-900 group-hover:text-blue-600">
|
||||
{{ $employee->display_name ?? $employee->user?->name ?? '-' }}
|
||||
</div>
|
||||
@if($employee->employee_code)
|
||||
<div class="text-xs text-gray-400">{{ $employee->employee_code }}</div>
|
||||
@endif
|
||||
</div>
|
||||
</a>
|
||||
</td>
|
||||
|
||||
{{-- 부서 --}}
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-700">
|
||||
{{ $employee->department?->name ?? '-' }}
|
||||
</td>
|
||||
|
||||
{{-- 직급/직책 --}}
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-700">
|
||||
<div>{{ $employee->position_label ?? '-' }}</div>
|
||||
@if($employee->job_title_label)
|
||||
<div class="text-xs text-gray-400">{{ $employee->job_title_label }}</div>
|
||||
@endif
|
||||
</td>
|
||||
|
||||
{{-- 상태 --}}
|
||||
<td class="px-6 py-4 whitespace-nowrap text-center">
|
||||
@switch($employee->employee_status)
|
||||
@case('active')
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-emerald-100 text-emerald-700">
|
||||
재직
|
||||
</span>
|
||||
@break
|
||||
@case('leave')
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-amber-100 text-amber-700">
|
||||
휴직
|
||||
</span>
|
||||
@break
|
||||
@case('resigned')
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-700">
|
||||
퇴직
|
||||
</span>
|
||||
@break
|
||||
@default
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-700">
|
||||
{{ $employee->employee_status ?? '-' }}
|
||||
</span>
|
||||
@endswitch
|
||||
</td>
|
||||
|
||||
{{-- 입사일 --}}
|
||||
<td class="px-6 py-4 whitespace-nowrap text-center text-sm text-gray-500">
|
||||
{{ $employee->hire_date ?? '-' }}
|
||||
</td>
|
||||
|
||||
{{-- 연락처 --}}
|
||||
<td class="px-6 py-4 whitespace-nowrap text-center text-sm text-gray-500">
|
||||
{{ $employee->user?->phone ?? $employee->user?->email ?? '-' }}
|
||||
</td>
|
||||
|
||||
{{-- 작업 --}}
|
||||
<td class="px-6 py-4 whitespace-nowrap text-center">
|
||||
<div class="flex items-center justify-center gap-2">
|
||||
{{-- 상세 --}}
|
||||
<a href="{{ route('hr.employees.show', $employee->id) }}"
|
||||
class="text-gray-600 hover:text-gray-800" title="상세">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
{{-- 수정 --}}
|
||||
<a href="{{ route('hr.employees.edit', $employee->id) }}"
|
||||
class="text-blue-600 hover:text-blue-800" title="수정">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
{{-- 퇴직 처리 --}}
|
||||
@if($employee->employee_status !== 'resigned')
|
||||
<button type="button"
|
||||
hx-delete="{{ route('api.admin.hr.employees.destroy', $employee->id) }}"
|
||||
hx-headers='{"X-CSRF-TOKEN": "{{ csrf_token() }}"}'
|
||||
hx-target="#employees-table"
|
||||
hx-swap="innerHTML"
|
||||
hx-confirm="{{ $employee->display_name ?? $employee->user?->name }}님을 퇴직 처리하시겠습니까?"
|
||||
class="text-red-600 hover:text-red-800" title="퇴직처리">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/>
|
||||
</svg>
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="7" class="px-6 py-12 text-center">
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<svg class="w-12 h-12 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/>
|
||||
</svg>
|
||||
<p class="text-gray-500">등록된 사원이 없습니다.</p>
|
||||
<a href="{{ route('hr.employees.create') }}"
|
||||
class="mt-2 text-sm text-blue-600 hover:text-blue-700 font-medium">
|
||||
첫 번째 사원 등록하기 →
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</x-table-swipe>
|
||||
|
||||
{{-- 페이지네이션 --}}
|
||||
@if($employees->hasPages())
|
||||
<div class="px-6 py-4 border-t border-gray-200 bg-gray-50">
|
||||
{{ $employees->links() }}
|
||||
</div>
|
||||
@endif
|
||||
152
resources/views/hr/employees/show.blade.php
Normal file
152
resources/views/hr/employees/show.blade.php
Normal file
@@ -0,0 +1,152 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', '사원 상세')
|
||||
|
||||
@section('content')
|
||||
<div class="container mx-auto px-4 py-6 max-w-3xl">
|
||||
{{-- 페이지 헤더 --}}
|
||||
<div class="mb-6">
|
||||
<a href="{{ route('hr.employees.index') }}" class="text-sm text-gray-500 hover:text-gray-700 inline-flex items-center gap-1 mb-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
||||
</svg>
|
||||
사원 목록으로
|
||||
</a>
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-2xl font-bold text-gray-800">사원 상세</h1>
|
||||
<a href="{{ route('hr.employees.edit', $employee->id) }}"
|
||||
class="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
|
||||
</svg>
|
||||
수정
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 프로필 카드 --}}
|
||||
<div class="bg-white rounded-lg shadow-sm p-6 mb-6">
|
||||
<div class="flex items-center gap-5">
|
||||
<div class="shrink-0 w-16 h-16 rounded-full bg-blue-100 text-blue-600 flex items-center justify-center text-2xl font-bold">
|
||||
{{ mb_substr($employee->display_name ?? $employee->user?->name ?? '?', 0, 1) }}
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-xl font-bold text-gray-900">
|
||||
{{ $employee->display_name ?? $employee->user?->name ?? '-' }}
|
||||
</h2>
|
||||
<div class="flex flex-wrap items-center gap-2 mt-1">
|
||||
@if($employee->employee_code)
|
||||
<span class="text-sm text-gray-500">{{ $employee->employee_code }}</span>
|
||||
<span class="text-gray-300">|</span>
|
||||
@endif
|
||||
@if($employee->department)
|
||||
<span class="text-sm text-gray-500">{{ $employee->department->name }}</span>
|
||||
<span class="text-gray-300">|</span>
|
||||
@endif
|
||||
<span class="text-sm text-gray-500">{{ $employee->position_label ?? '-' }}</span>
|
||||
@if($employee->job_title_label)
|
||||
<span class="text-gray-300">|</span>
|
||||
<span class="text-sm text-gray-500">{{ $employee->job_title_label }}</span>
|
||||
@endif
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
@switch($employee->employee_status)
|
||||
@case('active')
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-emerald-100 text-emerald-700">재직</span>
|
||||
@break
|
||||
@case('leave')
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-amber-100 text-amber-700">휴직</span>
|
||||
@break
|
||||
@case('resigned')
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-700">퇴직</span>
|
||||
@break
|
||||
@endswitch
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 상세 정보 --}}
|
||||
<div class="bg-white rounded-lg shadow-sm overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h3 class="text-lg font-semibold text-gray-800">상세 정보</h3>
|
||||
</div>
|
||||
<div class="divide-y divide-gray-100">
|
||||
{{-- 기본 정보 --}}
|
||||
<div class="px-6 py-3 flex" style="flex-wrap: wrap;">
|
||||
<div class="shrink-0 text-sm font-medium text-gray-500" style="width: 140px;">이름</div>
|
||||
<div class="text-sm text-gray-900">{{ $employee->user?->name ?? '-' }}</div>
|
||||
</div>
|
||||
<div class="px-6 py-3 flex" style="flex-wrap: wrap;">
|
||||
<div class="shrink-0 text-sm font-medium text-gray-500" style="width: 140px;">표시 이름</div>
|
||||
<div class="text-sm text-gray-900">{{ $employee->display_name ?? '-' }}</div>
|
||||
</div>
|
||||
<div class="px-6 py-3 flex" style="flex-wrap: wrap;">
|
||||
<div class="shrink-0 text-sm font-medium text-gray-500" style="width: 140px;">사번</div>
|
||||
<div class="text-sm text-gray-900">{{ $employee->employee_code ?? '-' }}</div>
|
||||
</div>
|
||||
<div class="px-6 py-3 flex" style="flex-wrap: wrap;">
|
||||
<div class="shrink-0 text-sm font-medium text-gray-500" style="width: 140px;">이메일</div>
|
||||
<div class="text-sm text-gray-900">{{ $employee->user?->email ?? '-' }}</div>
|
||||
</div>
|
||||
<div class="px-6 py-3 flex" style="flex-wrap: wrap;">
|
||||
<div class="shrink-0 text-sm font-medium text-gray-500" style="width: 140px;">연락처</div>
|
||||
<div class="text-sm text-gray-900">{{ $employee->user?->phone ?? '-' }}</div>
|
||||
</div>
|
||||
|
||||
{{-- 근무 정보 --}}
|
||||
<div class="px-6 py-4 bg-gray-50">
|
||||
<span class="text-sm font-semibold text-gray-600">근무 정보</span>
|
||||
</div>
|
||||
<div class="px-6 py-3 flex" style="flex-wrap: wrap;">
|
||||
<div class="shrink-0 text-sm font-medium text-gray-500" style="width: 140px;">부서</div>
|
||||
<div class="text-sm text-gray-900">{{ $employee->department?->name ?? '-' }}</div>
|
||||
</div>
|
||||
<div class="px-6 py-3 flex" style="flex-wrap: wrap;">
|
||||
<div class="shrink-0 text-sm font-medium text-gray-500" style="width: 140px;">직급</div>
|
||||
<div class="text-sm text-gray-900">{{ $employee->position_label ?? '-' }}</div>
|
||||
</div>
|
||||
<div class="px-6 py-3 flex" style="flex-wrap: wrap;">
|
||||
<div class="shrink-0 text-sm font-medium text-gray-500" style="width: 140px;">직책</div>
|
||||
<div class="text-sm text-gray-900">{{ $employee->job_title_label ?? '-' }}</div>
|
||||
</div>
|
||||
<div class="px-6 py-3 flex" style="flex-wrap: wrap;">
|
||||
<div class="shrink-0 text-sm font-medium text-gray-500" style="width: 140px;">입사일</div>
|
||||
<div class="text-sm text-gray-900">{{ $employee->hire_date ?? '-' }}</div>
|
||||
</div>
|
||||
<div class="px-6 py-3 flex" style="flex-wrap: wrap;">
|
||||
<div class="shrink-0 text-sm font-medium text-gray-500" style="width: 140px;">재직상태</div>
|
||||
<div class="text-sm text-gray-900">
|
||||
@switch($employee->employee_status)
|
||||
@case('active') 재직 @break
|
||||
@case('leave') 휴직 @break
|
||||
@case('resigned') 퇴직 @break
|
||||
@default {{ $employee->employee_status }} @break
|
||||
@endswitch
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 추가 정보 --}}
|
||||
<div class="px-6 py-4 bg-gray-50">
|
||||
<span class="text-sm font-semibold text-gray-600">추가 정보</span>
|
||||
</div>
|
||||
<div class="px-6 py-3 flex" style="flex-wrap: wrap;">
|
||||
<div class="shrink-0 text-sm font-medium text-gray-500" style="width: 140px;">주소</div>
|
||||
<div class="text-sm text-gray-900">{{ $employee->address ?? '-' }}</div>
|
||||
</div>
|
||||
<div class="px-6 py-3 flex" style="flex-wrap: wrap;">
|
||||
<div class="shrink-0 text-sm font-medium text-gray-500" style="width: 140px;">비상연락처</div>
|
||||
<div class="text-sm text-gray-900">{{ $employee->emergency_contact ?? '-' }}</div>
|
||||
</div>
|
||||
<div class="px-6 py-3 flex" style="flex-wrap: wrap;">
|
||||
<div class="shrink-0 text-sm font-medium text-gray-500" style="width: 140px;">등록일</div>
|
||||
<div class="text-sm text-gray-900">{{ $employee->created_at?->format('Y-m-d H:i') ?? '-' }}</div>
|
||||
</div>
|
||||
<div class="px-6 py-3 flex" style="flex-wrap: wrap;">
|
||||
<div class="shrink-0 text-sm font-medium text-gray-500" style="width: 140px;">수정일</div>
|
||||
<div class="text-sm text-gray-900">{{ $employee->updated_at?->format('Y-m-d H:i') ?? '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -1034,3 +1034,18 @@
|
||||
// 요약 결과 조회 (HTMX 지원)
|
||||
Route::get('/{id}/summary', [MeetingLogController::class, 'summary'])->name('summary');
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| HR (인사관리) API
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
Route::middleware(['web', 'auth', 'hq.member'])->prefix('admin/hr/employees')->name('api.admin.hr.employees.')->group(function () {
|
||||
Route::get('/stats', [\App\Http\Controllers\Api\Admin\HR\EmployeeController::class, 'stats'])->name('stats');
|
||||
Route::get('/', [\App\Http\Controllers\Api\Admin\HR\EmployeeController::class, 'index'])->name('index');
|
||||
Route::post('/', [\App\Http\Controllers\Api\Admin\HR\EmployeeController::class, 'store'])->name('store');
|
||||
Route::get('/{id}', [\App\Http\Controllers\Api\Admin\HR\EmployeeController::class, 'show'])->name('show');
|
||||
Route::put('/{id}', [\App\Http\Controllers\Api\Admin\HR\EmployeeController::class, 'update'])->name('update');
|
||||
Route::delete('/{id}', [\App\Http\Controllers\Api\Admin\HR\EmployeeController::class, 'destroy'])->name('destroy');
|
||||
});
|
||||
|
||||
|
||||
@@ -883,6 +883,21 @@
|
||||
return response()->file(public_path('일일자금일보.html'));
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| HR Routes (인사관리)
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
Route::middleware('auth')->prefix('hr')->name('hr.')->group(function () {
|
||||
// 사원관리
|
||||
Route::prefix('employees')->name('employees.')->group(function () {
|
||||
Route::get('/', [\App\Http\Controllers\HR\EmployeeController::class, 'index'])->name('index');
|
||||
Route::get('/create', [\App\Http\Controllers\HR\EmployeeController::class, 'create'])->name('create');
|
||||
Route::get('/{id}', [\App\Http\Controllers\HR\EmployeeController::class, 'show'])->name('show');
|
||||
Route::get('/{id}/edit', [\App\Http\Controllers\HR\EmployeeController::class, 'edit'])->name('edit');
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Finance Routes (재무 관리)
|
||||
|
||||
Reference in New Issue
Block a user