- deduction_overrides validation에서 min:0 제거 (마이너스 허용) - 수정 모달에서 calculate API 대신 DB 저장값 직접 표시
974 lines
50 KiB
PHP
974 lines
50 KiB
PHP
@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>
|
|
<div class="flex items-center gap-2 mt-1">
|
|
<select id="payrollYear" class="px-2 py-1 border border-gray-300 rounded text-sm text-gray-600 focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
|
@for($y = now()->year; $y >= now()->year - 2; $y--)
|
|
<option value="{{ $y }}" {{ $stats['year'] == $y ? 'selected' : '' }}>{{ $y }}년</option>
|
|
@endfor
|
|
</select>
|
|
<select id="payrollMonth" class="px-2 py-1 border border-gray-300 rounded text-sm text-gray-600 focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
|
@for($m = 1; $m <= 12; $m++)
|
|
<option value="{{ $m }}" {{ $stats['month'] == $m ? 'selected' : '' }}>{{ $m }}월</option>
|
|
@endfor
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="flex flex-wrap items-center gap-2 sm:gap-3">
|
|
<button type="button" onclick="openCreatePayrollModal()"
|
|
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 6v6m0 0v6m0-6h6m-6 0H6"/>
|
|
</svg>
|
|
급여 등록
|
|
</button>
|
|
<button type="button" onclick="bulkGeneratePayrolls()"
|
|
class="inline-flex items-center gap-2 px-4 py-2 bg-purple-600 hover:bg-purple-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="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
|
|
</svg>
|
|
일괄 생성
|
|
</button>
|
|
<button type="button" onclick="exportPayrolls()"
|
|
class="inline-flex items-center gap-2 px-4 py-2 bg-emerald-600 hover:bg-emerald-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 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
|
</svg>
|
|
엑셀 다운로드
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- 탭 네비게이션 --}}
|
|
<div class="flex items-center gap-1 mb-4 border-b border-gray-200">
|
|
<button type="button" onclick="switchTab('list')" id="tab-list"
|
|
class="px-4 py-2.5 text-sm font-medium border-b-2 transition-colors border-blue-600 text-blue-600">
|
|
급여 목록
|
|
</button>
|
|
<button type="button" onclick="switchTab('settings')" id="tab-settings"
|
|
class="px-4 py-2.5 text-sm font-medium border-b-2 transition-colors border-transparent text-gray-500 hover:text-gray-700">
|
|
급여 설정
|
|
</button>
|
|
</div>
|
|
|
|
{{-- 통계 카드 (HTMX 갱신 대상) --}}
|
|
<div id="stats-container" class="mb-4">
|
|
@include('hr.payrolls.partials.stats', ['stats' => $stats])
|
|
</div>
|
|
|
|
{{-- 탭 콘텐츠 영역 --}}
|
|
<div id="payrolls-content">
|
|
{{-- 급여 목록 탭 --}}
|
|
<div id="content-list">
|
|
<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="payrollFilter">
|
|
<form id="payrollFilterForm" class="flex flex-wrap gap-3 items-end">
|
|
<div style="flex: 1 1 180px; max-width: 260px;">
|
|
<label class="block text-xs text-gray-500 mb-1">검색</label>
|
|
<input type="text" name="q" placeholder="사원 이름..."
|
|
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 }}">{{ $dept->name }}</option>
|
|
@endforeach
|
|
</select>
|
|
</div>
|
|
<div style="flex: 0 1 140px;">
|
|
<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>
|
|
@foreach($statusMap as $key => $label)
|
|
<option value="{{ $key }}">{{ $label }}</option>
|
|
@endforeach
|
|
</select>
|
|
</div>
|
|
<div class="shrink-0">
|
|
<button type="submit"
|
|
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="payrolls-table"
|
|
hx-get="{{ route('api.admin.hr.payrolls.index') }}"
|
|
hx-vals='{"year": "{{ $stats['year'] }}", "month": "{{ $stats['month'] }}"}'
|
|
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>
|
|
|
|
{{-- 급여 설정 탭 --}}
|
|
<div id="content-settings" class="hidden">
|
|
<div id="payroll-settings-container">
|
|
@include('hr.payrolls.partials.settings', ['settings' => $settings])
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- 급여 등록/수정 모달 --}}
|
|
<div id="payrollModal" class="fixed inset-0 z-50 hidden">
|
|
<div class="fixed inset-0 bg-black/40" onclick="closePayrollModal()"></div>
|
|
<div class="fixed inset-0 flex items-center justify-center p-4 overflow-y-auto">
|
|
<div class="bg-white rounded-lg shadow-xl w-full max-w-2xl relative my-8">
|
|
<div class="flex items-center justify-between px-6 py-4 border-b border-gray-200">
|
|
<h3 id="payrollModalTitle" class="text-lg font-semibold text-gray-800">급여 등록</h3>
|
|
<button type="button" onclick="closePayrollModal()" class="text-gray-400 hover:text-gray-600">
|
|
<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="M6 18L18 6M6 6l12 12"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
<form id="payrollForm" class="px-6 py-4 space-y-4 max-h-[70vh] overflow-y-auto">
|
|
<input type="hidden" id="payrollId" name="id" value="">
|
|
|
|
{{-- 기본 정보 --}}
|
|
<div class="grid gap-4" style="grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));">
|
|
<div>
|
|
<label class="block text-xs text-gray-500 mb-1">사원 <span class="text-red-500">*</span></label>
|
|
<select id="payrollUserId" name="user_id" required
|
|
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($employees as $emp)
|
|
<option value="{{ $emp->user_id }}"
|
|
data-salary="{{ $emp->getJsonExtraValue('salary', 0) }}"
|
|
data-dependents="{{ 1 + collect($emp->dependents)->where('is_dependent', true)->count() }}">
|
|
{{ $emp->display_name ?? $emp->user?->name }}
|
|
</option>
|
|
@endforeach
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs text-gray-500 mb-1">급여 연월</label>
|
|
<div class="flex gap-2">
|
|
<input type="number" id="payrollPayYear" name="pay_year" min="2020" max="2100"
|
|
value="{{ now()->year }}"
|
|
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">
|
|
<input type="number" id="payrollPayMonth" name="pay_month" min="1" max="12"
|
|
value="{{ now()->month }}"
|
|
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>
|
|
</div>
|
|
|
|
{{-- 지급 항목 --}}
|
|
<div>
|
|
<h4 class="text-sm font-medium text-gray-700 mb-2">지급 항목</h4>
|
|
<div class="grid gap-3" style="grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));">
|
|
<div>
|
|
<label class="block text-xs text-gray-500 mb-1">기본급</label>
|
|
<input type="text" id="payrollBaseSalary" name="base_salary" inputmode="numeric" value="0"
|
|
oninput="formatMoneyInput(this); recalculate()"
|
|
onfocus="moneyFocus(this)" onblur="moneyBlur(this)"
|
|
class="money-input w-full px-3 py-2 border border-gray-300 rounded-lg text-sm text-right focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs text-gray-500 mb-1">고정연장근로수당</label>
|
|
<input type="text" id="payrollOvertimePay" name="overtime_pay" inputmode="numeric" value="0"
|
|
oninput="formatMoneyInput(this); recalculate()"
|
|
onfocus="moneyFocus(this)" onblur="moneyBlur(this)"
|
|
class="money-input w-full px-3 py-2 border border-gray-300 rounded-lg text-sm text-right focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs text-gray-500 mb-1">식대(비과세)</label>
|
|
<input type="text" id="payrollBonus" name="bonus" inputmode="numeric" value="0"
|
|
oninput="formatMoneyInput(this); recalculate()"
|
|
onfocus="moneyFocus(this)" onblur="moneyBlur(this)"
|
|
class="money-input w-full px-3 py-2 border border-gray-300 rounded-lg text-sm text-right focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- 수당 (동적 추가) --}}
|
|
<div>
|
|
<div class="flex items-center justify-between mb-2">
|
|
<h4 class="text-sm font-medium text-gray-700">수당</h4>
|
|
<button type="button" onclick="addAllowanceRow()" class="text-xs text-blue-600 hover:text-blue-800">+ 추가</button>
|
|
</div>
|
|
<div id="allowancesContainer" class="space-y-2"></div>
|
|
</div>
|
|
|
|
{{-- 공제 항목 (자동 계산 + 수동 수정 가능) --}}
|
|
<div>
|
|
<div class="flex items-center justify-between mb-2">
|
|
<h4 class="text-sm font-medium text-gray-700">공제 항목</h4>
|
|
<button type="button" onclick="resetDeductionOverrides()" class="text-xs text-blue-600 hover:text-blue-800">재계산</button>
|
|
</div>
|
|
<div class="bg-gray-50 rounded-lg p-3 space-y-1.5 text-sm">
|
|
<div class="flex items-center justify-between">
|
|
<span class="text-gray-500">국민연금</span>
|
|
<input type="text" id="calcPension" inputmode="numeric" value="0"
|
|
oninput="formatMoneyInput(this); markManualOverride(this); updateDeductionTotals()"
|
|
onfocus="moneyFocus(this)" onblur="moneyBlur(this)"
|
|
class="money-input w-28 px-2 py-1 border border-gray-200 rounded text-right text-sm text-gray-700 focus:ring-1 focus:ring-blue-500 focus:border-blue-500">
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<span class="text-gray-500">건강보험</span>
|
|
<input type="text" id="calcHealth" inputmode="numeric" value="0"
|
|
oninput="formatMoneyInput(this); markManualOverride(this); updateDeductionTotals()"
|
|
onfocus="moneyFocus(this)" onblur="moneyBlur(this)"
|
|
class="money-input w-28 px-2 py-1 border border-gray-200 rounded text-right text-sm text-gray-700 focus:ring-1 focus:ring-blue-500 focus:border-blue-500">
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<span class="text-gray-500">장기요양보험</span>
|
|
<input type="text" id="calcLongTermCare" inputmode="numeric" value="0"
|
|
oninput="formatMoneyInput(this); markManualOverride(this); updateDeductionTotals()"
|
|
onfocus="moneyFocus(this)" onblur="moneyBlur(this)"
|
|
class="money-input w-28 px-2 py-1 border border-gray-200 rounded text-right text-sm text-gray-700 focus:ring-1 focus:ring-blue-500 focus:border-blue-500">
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<span class="text-gray-500">고용보험</span>
|
|
<input type="text" id="calcEmployment" inputmode="numeric" value="0"
|
|
oninput="formatMoneyInput(this); markManualOverride(this); updateDeductionTotals()"
|
|
onfocus="moneyFocus(this)" onblur="moneyBlur(this)"
|
|
class="money-input w-28 px-2 py-1 border border-gray-200 rounded text-right text-sm text-gray-700 focus:ring-1 focus:ring-blue-500 focus:border-blue-500">
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<span class="text-gray-500">근로소득세</span>
|
|
<input type="text" id="calcIncomeTax" inputmode="numeric" value="0"
|
|
oninput="formatMoneyInput(this); markManualOverride(this); updateDeductionTotals()"
|
|
onfocus="moneyFocus(this)" onblur="moneyBlur(this)"
|
|
class="money-input w-28 px-2 py-1 border border-gray-200 rounded text-right text-sm text-gray-700 focus:ring-1 focus:ring-blue-500 focus:border-blue-500">
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<span class="text-gray-500">지방소득세</span>
|
|
<input type="text" id="calcResidentTax" inputmode="numeric" value="0"
|
|
oninput="formatMoneyInput(this); markManualOverride(this); updateDeductionTotals()"
|
|
onfocus="moneyFocus(this)" onblur="moneyBlur(this)"
|
|
class="money-input w-28 px-2 py-1 border border-gray-200 rounded text-right text-sm text-gray-700 focus:ring-1 focus:ring-blue-500 focus:border-blue-500">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- 추가 공제 (동적) --}}
|
|
<div>
|
|
<div class="flex items-center justify-between mb-2">
|
|
<h4 class="text-sm font-medium text-gray-700">추가 공제</h4>
|
|
<button type="button" onclick="addDeductionRow()" class="text-xs text-blue-600 hover:text-blue-800">+ 추가</button>
|
|
</div>
|
|
<div id="deductionsContainer" class="space-y-2"></div>
|
|
</div>
|
|
|
|
{{-- 합계 --}}
|
|
<div class="bg-blue-50 rounded-lg p-4 space-y-2">
|
|
<div class="flex justify-between text-sm">
|
|
<span class="text-gray-600 font-medium">총 지급액</span>
|
|
<span id="calcGross" class="text-blue-700 font-bold">0</span>
|
|
</div>
|
|
<div class="flex justify-between text-xs">
|
|
<span class="text-gray-400">과세표준 (식대 제외)</span>
|
|
<span id="calcTaxableBase" class="text-gray-500">0</span>
|
|
</div>
|
|
<div class="flex justify-between text-xs">
|
|
<span class="text-gray-400">공제대상가족수</span>
|
|
<span id="calcFamilyCount" class="text-gray-500">1명</span>
|
|
</div>
|
|
<div class="flex justify-between text-sm">
|
|
<span class="text-gray-600 font-medium">총 공제액</span>
|
|
<span id="calcTotalDeductions" class="text-red-600 font-bold">0</span>
|
|
</div>
|
|
<div class="flex justify-between text-base border-t border-blue-200 pt-2">
|
|
<span class="text-gray-800 font-bold">실수령액</span>
|
|
<span id="calcNet" class="text-emerald-600 font-bold">0</span>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- 비고 --}}
|
|
<div>
|
|
<label class="block text-xs text-gray-500 mb-1">비고</label>
|
|
<textarea id="payrollNote" name="note" rows="2"
|
|
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"></textarea>
|
|
</div>
|
|
</form>
|
|
<div class="flex items-center justify-end gap-3 px-6 py-4 border-t border-gray-200">
|
|
<button type="button" onclick="closePayrollModal()"
|
|
class="px-4 py-2 text-sm text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors">
|
|
취소
|
|
</button>
|
|
<button type="button" onclick="submitPayroll()"
|
|
class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors">
|
|
저장
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- 급여 상세 모달 --}}
|
|
<div id="payrollDetailModal" class="fixed inset-0 z-50 hidden">
|
|
<div class="fixed inset-0 bg-black/40" onclick="closePayrollDetailModal()"></div>
|
|
<div class="fixed inset-0 flex items-center justify-center p-4 overflow-y-auto">
|
|
<div class="bg-white rounded-lg shadow-xl w-full max-w-lg relative my-8">
|
|
<div class="flex items-center justify-between px-6 py-4 border-b border-gray-200">
|
|
<h3 class="text-lg font-semibold text-gray-800">급여 상세</h3>
|
|
<button type="button" onclick="closePayrollDetailModal()" class="text-gray-400 hover:text-gray-600">
|
|
<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="M6 18L18 6M6 6l12 12"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
<div id="payrollDetailContent" class="px-6 py-4 max-h-[70vh] overflow-y-auto"></div>
|
|
<div class="flex items-center justify-end px-6 py-4 border-t border-gray-200">
|
|
<button type="button" onclick="closePayrollDetailModal()"
|
|
class="px-4 py-2 text-sm text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors">
|
|
닫기
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endsection
|
|
|
|
@push('scripts')
|
|
<script>
|
|
// ===== 현재 탭 상태 =====
|
|
let currentTab = 'list';
|
|
const tabLoaded = { list: true, settings: true };
|
|
|
|
function switchTab(tab) {
|
|
document.getElementById('tab-' + currentTab).className = 'px-4 py-2.5 text-sm font-medium border-b-2 transition-colors border-transparent text-gray-500 hover:text-gray-700';
|
|
document.getElementById('content-' + currentTab).classList.add('hidden');
|
|
|
|
currentTab = tab;
|
|
document.getElementById('tab-' + tab).className = 'px-4 py-2.5 text-sm font-medium border-b-2 transition-colors border-blue-600 text-blue-600';
|
|
document.getElementById('content-' + tab).classList.remove('hidden');
|
|
}
|
|
|
|
// ===== 연월 변경 =====
|
|
document.getElementById('payrollYear').addEventListener('change', onPeriodChange);
|
|
document.getElementById('payrollMonth').addEventListener('change', onPeriodChange);
|
|
|
|
function onPeriodChange() {
|
|
refreshStats();
|
|
refreshTable();
|
|
}
|
|
|
|
function refreshStats() {
|
|
htmx.ajax('GET', '{{ route("api.admin.hr.payrolls.stats") }}', {
|
|
target: '#stats-container',
|
|
swap: 'innerHTML',
|
|
values: {
|
|
year: document.getElementById('payrollYear').value,
|
|
month: document.getElementById('payrollMonth').value,
|
|
},
|
|
});
|
|
}
|
|
|
|
// ===== 필터 =====
|
|
document.getElementById('payrollFilterForm')?.addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
refreshTable();
|
|
});
|
|
|
|
function refreshTable() {
|
|
htmx.ajax('GET', '{{ route("api.admin.hr.payrolls.index") }}', {
|
|
target: '#payrolls-table',
|
|
swap: 'innerHTML',
|
|
values: getFilterValues(),
|
|
});
|
|
}
|
|
|
|
function getFilterValues() {
|
|
const form = document.getElementById('payrollFilterForm');
|
|
const formData = new FormData(form);
|
|
const values = {
|
|
year: document.getElementById('payrollYear').value,
|
|
month: document.getElementById('payrollMonth').value,
|
|
};
|
|
for (const [key, value] of formData.entries()) {
|
|
if (value) values[key] = value;
|
|
}
|
|
return values;
|
|
}
|
|
|
|
// ===== 엑셀 다운로드 =====
|
|
function exportPayrolls() {
|
|
const params = new URLSearchParams(getFilterValues());
|
|
window.location.href = '{{ route("api.admin.hr.payrolls.export") }}?' + params.toString();
|
|
}
|
|
|
|
// ===== 급여 등록 모달 =====
|
|
let editingPayrollId = null;
|
|
|
|
function openCreatePayrollModal() {
|
|
editingPayrollId = null;
|
|
document.getElementById('payrollModalTitle').textContent = '급여 등록';
|
|
document.getElementById('payrollForm').reset();
|
|
document.getElementById('payrollId').value = '';
|
|
document.getElementById('payrollPayYear').value = document.getElementById('payrollYear').value;
|
|
document.getElementById('payrollPayMonth').value = document.getElementById('payrollMonth').value;
|
|
document.getElementById('allowancesContainer').innerHTML = '';
|
|
document.getElementById('deductionsContainer').innerHTML = '';
|
|
resetCalculation();
|
|
document.getElementById('payrollModal').classList.remove('hidden');
|
|
}
|
|
|
|
function openEditPayrollModal(id, data) {
|
|
editingPayrollId = id;
|
|
document.getElementById('payrollModalTitle').textContent = '급여 수정';
|
|
document.getElementById('payrollId').value = id;
|
|
document.getElementById('payrollUserId').value = data.user_id;
|
|
document.getElementById('payrollUserId').disabled = true;
|
|
document.getElementById('payrollPayYear').value = document.getElementById('payrollYear').value;
|
|
document.getElementById('payrollPayMonth').value = document.getElementById('payrollMonth').value;
|
|
setMoneyValue(document.getElementById('payrollBaseSalary'), data.base_salary || 0);
|
|
setMoneyValue(document.getElementById('payrollOvertimePay'), data.overtime_pay || 0);
|
|
setMoneyValue(document.getElementById('payrollBonus'), data.bonus || 0);
|
|
document.getElementById('payrollNote').value = data.note || '';
|
|
|
|
// 수당 복원
|
|
document.getElementById('allowancesContainer').innerHTML = '';
|
|
if (data.allowances && data.allowances.length > 0) {
|
|
data.allowances.forEach(a => addAllowanceRow(a.name, a.amount));
|
|
}
|
|
|
|
// 공제 복원
|
|
document.getElementById('deductionsContainer').innerHTML = '';
|
|
if (data.deductions && data.deductions.length > 0) {
|
|
data.deductions.forEach(d => addDeductionRow(d.name, d.amount));
|
|
}
|
|
|
|
// 법정공제 항목: DB에 저장된 값을 그대로 표시
|
|
const savedFields = {
|
|
'calcPension': data.pension, 'calcHealth': data.health_insurance,
|
|
'calcLongTermCare': data.long_term_care, 'calcEmployment': data.employment_insurance,
|
|
'calcIncomeTax': data.income_tax, 'calcResidentTax': data.resident_tax,
|
|
};
|
|
Object.entries(savedFields).forEach(([id, val]) => {
|
|
setMoneyValue(document.getElementById(id), val || 0);
|
|
});
|
|
|
|
// 총 지급액·과세표준 계산 표시
|
|
const baseSalary = data.base_salary || 0;
|
|
const overtimePay = data.overtime_pay || 0;
|
|
const bonus = data.bonus || 0;
|
|
let allowancesTotal = 0;
|
|
if (data.allowances) data.allowances.forEach(a => allowancesTotal += (a.amount || 0));
|
|
const grossSalary = baseSalary + overtimePay + bonus + allowancesTotal;
|
|
document.getElementById('calcGross').textContent = numberFormat(grossSalary);
|
|
document.getElementById('calcTaxableBase').textContent = numberFormat(grossSalary - bonus);
|
|
updateDeductionTotals();
|
|
|
|
document.getElementById('payrollModal').classList.remove('hidden');
|
|
}
|
|
|
|
function closePayrollModal() {
|
|
document.getElementById('payrollModal').classList.add('hidden');
|
|
document.getElementById('payrollUserId').disabled = false;
|
|
}
|
|
|
|
// ===== 사원 선택 시 기본급 자동 입력 + 가족수 표시 =====
|
|
document.getElementById('payrollUserId').addEventListener('change', function() {
|
|
const selected = this.options[this.selectedIndex];
|
|
const dependents = parseInt(selected.dataset.dependents || 1);
|
|
const fcEl = document.getElementById('calcFamilyCount');
|
|
if (fcEl) fcEl.textContent = dependents + '명';
|
|
|
|
if (editingPayrollId) return;
|
|
const salary = parseInt(selected.dataset.salary || 0);
|
|
if (salary > 0) {
|
|
setMoneyValue(document.getElementById('payrollBaseSalary'), Math.round(salary / 12));
|
|
recalculate();
|
|
}
|
|
});
|
|
|
|
// ===== 수당/공제 동적 행 =====
|
|
function addAllowanceRow(name, amount) {
|
|
const container = document.getElementById('allowancesContainer');
|
|
const div = document.createElement('div');
|
|
div.className = 'flex gap-2 items-center';
|
|
const formattedAmount = amount ? Number(amount).toLocaleString('ko-KR') : '';
|
|
div.innerHTML = `
|
|
<input type="text" placeholder="수당명" value="${name || ''}" class="allowance-name px-3 py-1.5 border border-gray-300 rounded-lg text-sm" style="flex: 1 1 120px;">
|
|
<input type="text" inputmode="numeric" placeholder="금액" value="${formattedAmount}" oninput="formatMoneyInput(this); recalculate()" onfocus="moneyFocus(this)" onblur="moneyBlur(this)" class="money-input allowance-amount px-3 py-1.5 border border-gray-300 rounded-lg text-sm text-right" style="flex: 0 0 120px;">
|
|
<button type="button" onclick="this.parentElement.remove(); recalculate();" class="shrink-0 text-red-400 hover:text-red-600">
|
|
<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="M6 18L18 6M6 6l12 12"/></svg>
|
|
</button>
|
|
`;
|
|
container.appendChild(div);
|
|
}
|
|
|
|
function addDeductionRow(name, amount) {
|
|
const container = document.getElementById('deductionsContainer');
|
|
const div = document.createElement('div');
|
|
div.className = 'flex gap-2 items-center';
|
|
const formattedAmount = amount ? Number(amount).toLocaleString('ko-KR') : '';
|
|
div.innerHTML = `
|
|
<input type="text" inputmode="numeric" placeholder="공제명" value="${name || ''}" class="deduction-name px-3 py-1.5 border border-gray-300 rounded-lg text-sm" style="flex: 1 1 120px;">
|
|
<input type="text" inputmode="numeric" placeholder="금액" value="${formattedAmount}" oninput="formatMoneyInput(this); recalculate()" onfocus="moneyFocus(this)" onblur="moneyBlur(this)" class="money-input deduction-amount px-3 py-1.5 border border-gray-300 rounded-lg text-sm text-right" style="flex: 0 0 120px;">
|
|
<button type="button" onclick="this.parentElement.remove(); recalculate();" class="shrink-0 text-red-400 hover:text-red-600">
|
|
<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="M6 18L18 6M6 6l12 12"/></svg>
|
|
</button>
|
|
`;
|
|
container.appendChild(div);
|
|
}
|
|
|
|
// ===== 자동 계산 (서버 호출) =====
|
|
let calcTimer = null;
|
|
function recalculate() {
|
|
clearTimeout(calcTimer);
|
|
calcTimer = setTimeout(doRecalculate, 300);
|
|
}
|
|
|
|
function doRecalculate() {
|
|
const allowances = [];
|
|
document.querySelectorAll('#allowancesContainer > div').forEach(row => {
|
|
const name = row.querySelector('.allowance-name').value;
|
|
const amount = parseMoneyValue(row.querySelector('.allowance-amount'));
|
|
if (name && amount > 0) allowances.push({name, amount});
|
|
});
|
|
|
|
const deductions = [];
|
|
document.querySelectorAll('#deductionsContainer > div').forEach(row => {
|
|
const name = row.querySelector('.deduction-name').value;
|
|
const amount = parseMoneyValue(row.querySelector('.deduction-amount'));
|
|
if (name && amount !== 0) deductions.push({name, amount});
|
|
});
|
|
|
|
const data = {
|
|
base_salary: parseMoneyValue(document.getElementById('payrollBaseSalary')),
|
|
overtime_pay: parseMoneyValue(document.getElementById('payrollOvertimePay')),
|
|
bonus: parseMoneyValue(document.getElementById('payrollBonus')),
|
|
allowances: allowances,
|
|
deductions: deductions,
|
|
user_id: parseInt(document.getElementById('payrollUserId').value) || null,
|
|
};
|
|
|
|
fetch('{{ route("api.admin.hr.payrolls.calculate") }}', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
|
'Accept': 'application/json',
|
|
},
|
|
body: JSON.stringify(data),
|
|
})
|
|
.then(r => r.json())
|
|
.then(result => {
|
|
if (result.success) {
|
|
const d = result.data;
|
|
const fields = {
|
|
'calcPension': d.pension,
|
|
'calcHealth': d.health_insurance,
|
|
'calcLongTermCare': d.long_term_care,
|
|
'calcEmployment': d.employment_insurance,
|
|
'calcIncomeTax': d.income_tax,
|
|
'calcResidentTax': d.resident_tax,
|
|
};
|
|
Object.entries(fields).forEach(([id, val]) => {
|
|
const el = document.getElementById(id);
|
|
if (!el.dataset.manual) setMoneyValue(el, val);
|
|
});
|
|
document.getElementById('calcGross').textContent = numberFormat(d.gross_salary);
|
|
document.getElementById('calcTaxableBase').textContent = numberFormat(d.taxable_base);
|
|
if (d.family_count) {
|
|
const fcEl = document.getElementById('calcFamilyCount');
|
|
if (fcEl) fcEl.textContent = d.family_count + '명';
|
|
}
|
|
updateDeductionTotals();
|
|
}
|
|
})
|
|
.catch(console.error);
|
|
}
|
|
|
|
function resetCalculation() {
|
|
['calcPension','calcHealth','calcLongTermCare','calcEmployment','calcIncomeTax','calcResidentTax'].forEach(id => {
|
|
const el = document.getElementById(id);
|
|
el.value = '0';
|
|
delete el.dataset.manual;
|
|
el.classList.remove('bg-yellow-50', 'border-yellow-300');
|
|
el.classList.add('border-gray-200');
|
|
});
|
|
['calcGross','calcTaxableBase','calcTotalDeductions','calcNet'].forEach(id => {
|
|
document.getElementById(id).textContent = '0';
|
|
});
|
|
}
|
|
|
|
function markManualOverride(el) {
|
|
el.dataset.manual = '1';
|
|
el.classList.remove('border-gray-200');
|
|
el.classList.add('bg-yellow-50', 'border-yellow-300');
|
|
}
|
|
|
|
function resetDeductionOverrides() {
|
|
['calcPension','calcHealth','calcLongTermCare','calcEmployment','calcIncomeTax','calcResidentTax'].forEach(id => {
|
|
const el = document.getElementById(id);
|
|
delete el.dataset.manual;
|
|
el.classList.remove('bg-yellow-50', 'border-yellow-300');
|
|
el.classList.add('border-gray-200');
|
|
});
|
|
recalculate();
|
|
}
|
|
|
|
function updateDeductionTotals() {
|
|
const ids = ['calcPension','calcHealth','calcLongTermCare','calcEmployment','calcIncomeTax','calcResidentTax'];
|
|
let statutory = 0;
|
|
ids.forEach(id => { statutory += parseMoneyValue(document.getElementById(id)); });
|
|
|
|
let extra = 0;
|
|
document.querySelectorAll('#deductionsContainer .deduction-amount').forEach(el => {
|
|
extra += parseMoneyValue(el);
|
|
});
|
|
|
|
const totalDeductions = statutory + extra;
|
|
const grossText = document.getElementById('calcGross').textContent;
|
|
const gross = parseFloat(grossText.replace(/,/g, '')) || 0;
|
|
const net = Math.max(0, gross - totalDeductions);
|
|
|
|
document.getElementById('calcTotalDeductions').textContent = numberFormat(totalDeductions);
|
|
document.getElementById('calcNet').textContent = numberFormat(net);
|
|
}
|
|
|
|
function numberFormat(n) {
|
|
return Number(n).toLocaleString('ko-KR');
|
|
}
|
|
|
|
// ===== 금액 입력 포맷팅 =====
|
|
function parseMoneyValue(el) {
|
|
if (typeof el === 'string') return parseFloat(el.replace(/,/g, '')) || 0;
|
|
return parseFloat((el.value || '').replace(/,/g, '')) || 0;
|
|
}
|
|
|
|
function formatMoneyInput(el) {
|
|
const pos = el.selectionStart;
|
|
const oldLen = el.value.length;
|
|
const isNeg = el.value.indexOf('-') === 0;
|
|
const raw = el.value.replace(/[^0-9]/g, '');
|
|
const num = parseInt(raw, 10);
|
|
if (isNaN(num)) { el.value = isNeg ? '-' : ''; return; }
|
|
el.value = (isNeg ? '-' : '') + num.toLocaleString('ko-KR');
|
|
const newLen = el.value.length;
|
|
const newPos = Math.max(0, pos + (newLen - oldLen));
|
|
el.setSelectionRange(newPos, newPos);
|
|
}
|
|
|
|
function moneyFocus(el) {
|
|
if (parseMoneyValue(el) === 0) el.value = '';
|
|
}
|
|
|
|
function moneyBlur(el) {
|
|
if (el.value.trim() === '') el.value = '0';
|
|
}
|
|
|
|
function setMoneyValue(el, val) {
|
|
const num = parseInt(val, 10) || 0;
|
|
el.value = num === 0 ? '0' : Number(num).toLocaleString('ko-KR');
|
|
}
|
|
|
|
// ===== 급여 저장 =====
|
|
function submitPayroll() {
|
|
const allowances = [];
|
|
document.querySelectorAll('#allowancesContainer > div').forEach(row => {
|
|
const name = row.querySelector('.allowance-name').value;
|
|
const amount = parseMoneyValue(row.querySelector('.allowance-amount'));
|
|
if (name && amount > 0) allowances.push({name, amount});
|
|
});
|
|
|
|
const deductions = [];
|
|
document.querySelectorAll('#deductionsContainer > div').forEach(row => {
|
|
const name = row.querySelector('.deduction-name').value;
|
|
const amount = parseMoneyValue(row.querySelector('.deduction-amount'));
|
|
if (name && amount !== 0) deductions.push({name, amount});
|
|
});
|
|
|
|
// 수동 수정된 공제 항목 override
|
|
const overrides = {};
|
|
const overrideMap = {
|
|
'calcPension': 'pension', 'calcHealth': 'health_insurance',
|
|
'calcLongTermCare': 'long_term_care', 'calcEmployment': 'employment_insurance',
|
|
'calcIncomeTax': 'income_tax', 'calcResidentTax': 'resident_tax',
|
|
};
|
|
Object.entries(overrideMap).forEach(([elId, field]) => {
|
|
const el = document.getElementById(elId);
|
|
if (el.dataset.manual) overrides[field] = parseMoneyValue(el);
|
|
});
|
|
|
|
const data = {
|
|
user_id: parseInt(document.getElementById('payrollUserId').value),
|
|
pay_year: parseInt(document.getElementById('payrollPayYear').value),
|
|
pay_month: parseInt(document.getElementById('payrollPayMonth').value),
|
|
base_salary: parseMoneyValue(document.getElementById('payrollBaseSalary')),
|
|
overtime_pay: parseMoneyValue(document.getElementById('payrollOvertimePay')),
|
|
bonus: parseMoneyValue(document.getElementById('payrollBonus')),
|
|
allowances: allowances.length > 0 ? allowances : null,
|
|
deductions: deductions.length > 0 ? deductions : null,
|
|
deduction_overrides: Object.keys(overrides).length > 0 ? overrides : null,
|
|
note: document.getElementById('payrollNote').value || null,
|
|
};
|
|
|
|
const isEdit = !!editingPayrollId;
|
|
const url = isEdit
|
|
? '{{ url("/api/admin/hr/payrolls") }}/' + editingPayrollId
|
|
: '{{ route("api.admin.hr.payrolls.store") }}';
|
|
|
|
fetch(url, {
|
|
method: isEdit ? 'PUT' : 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
|
'Accept': 'application/json',
|
|
},
|
|
body: JSON.stringify(data),
|
|
})
|
|
.then(r => r.json())
|
|
.then(result => {
|
|
if (result.success) {
|
|
closePayrollModal();
|
|
refreshTable();
|
|
refreshStats();
|
|
showToast(result.message, 'success');
|
|
} else {
|
|
showToast(result.message || '오류가 발생했습니다.', 'error');
|
|
}
|
|
})
|
|
.catch(err => {
|
|
console.error(err);
|
|
showToast('오류가 발생했습니다.', 'error');
|
|
});
|
|
}
|
|
|
|
// ===== 급여 삭제 =====
|
|
function deletePayroll(id) {
|
|
if (!confirm('이 급여를 삭제하시겠습니까?')) return;
|
|
|
|
fetch('{{ url("/api/admin/hr/payrolls") }}/' + id, {
|
|
method: 'DELETE',
|
|
headers: {
|
|
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
|
'Accept': 'application/json',
|
|
},
|
|
})
|
|
.then(r => r.json())
|
|
.then(result => {
|
|
if (result.success) {
|
|
refreshTable();
|
|
refreshStats();
|
|
showToast(result.message, 'success');
|
|
} else {
|
|
showToast(result.message, 'error');
|
|
}
|
|
})
|
|
.catch(err => {
|
|
console.error(err);
|
|
showToast('삭제 중 오류가 발생했습니다.', 'error');
|
|
});
|
|
}
|
|
|
|
// ===== 급여 확정 =====
|
|
function confirmPayroll(id) {
|
|
if (!confirm('이 급여를 확정하시겠습니까? 확정 후에는 수정할 수 없습니다.')) return;
|
|
|
|
fetch('{{ url("/api/admin/hr/payrolls") }}/' + id + '/confirm', {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
|
'Accept': 'application/json',
|
|
},
|
|
})
|
|
.then(r => r.json())
|
|
.then(result => {
|
|
if (result.success) {
|
|
refreshTable();
|
|
refreshStats();
|
|
showToast(result.message, 'success');
|
|
} else {
|
|
showToast(result.message, 'error');
|
|
}
|
|
})
|
|
.catch(err => {
|
|
console.error(err);
|
|
showToast('확정 중 오류가 발생했습니다.', 'error');
|
|
});
|
|
}
|
|
|
|
// ===== 급여 지급 =====
|
|
function payPayroll(id) {
|
|
if (!confirm('이 급여를 지급 처리하시겠습니까?')) return;
|
|
|
|
fetch('{{ url("/api/admin/hr/payrolls") }}/' + id + '/pay', {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
|
'Accept': 'application/json',
|
|
},
|
|
})
|
|
.then(r => r.json())
|
|
.then(result => {
|
|
if (result.success) {
|
|
refreshTable();
|
|
refreshStats();
|
|
showToast(result.message, 'success');
|
|
} else {
|
|
showToast(result.message, 'error');
|
|
}
|
|
})
|
|
.catch(err => {
|
|
console.error(err);
|
|
showToast('지급 처리 중 오류가 발생했습니다.', 'error');
|
|
});
|
|
}
|
|
|
|
// ===== 일괄 생성 =====
|
|
function bulkGeneratePayrolls() {
|
|
const year = document.getElementById('payrollYear').value;
|
|
const month = document.getElementById('payrollMonth').value;
|
|
|
|
if (!confirm(`${year}년 ${month}월 재직 사원 전체의 급여를 일괄 생성하시겠습니까?`)) return;
|
|
|
|
fetch('{{ route("api.admin.hr.payrolls.bulk-generate") }}', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
|
'Accept': 'application/json',
|
|
},
|
|
body: JSON.stringify({ year: parseInt(year), month: parseInt(month) }),
|
|
})
|
|
.then(r => r.json())
|
|
.then(result => {
|
|
if (result.success) {
|
|
refreshTable();
|
|
refreshStats();
|
|
showToast(result.message, 'success');
|
|
} else {
|
|
showToast(result.message, 'error');
|
|
}
|
|
})
|
|
.catch(err => {
|
|
console.error(err);
|
|
showToast('일괄 생성 중 오류가 발생했습니다.', 'error');
|
|
});
|
|
}
|
|
|
|
// ===== 급여 상세 모달 =====
|
|
function openPayrollDetail(id, data) {
|
|
let html = '<div class="space-y-4">';
|
|
html += `<div class="flex items-center gap-3 mb-2">
|
|
<div class="w-10 h-10 rounded-full bg-blue-100 text-blue-600 flex items-center justify-center font-medium">${data.user_name.charAt(0)}</div>
|
|
<div>
|
|
<div class="font-medium text-gray-900">${data.user_name}</div>
|
|
<div class="text-sm text-gray-500">${data.department} / ${data.period}</div>
|
|
</div>
|
|
</div>`;
|
|
|
|
html += '<div class="border rounded-lg overflow-hidden"><table class="w-full text-sm">';
|
|
html += '<tr class="bg-blue-50"><th class="px-4 py-2 text-left text-blue-800 font-medium" colspan="2">지급 항목</th></tr>';
|
|
html += `<tr class="border-t"><td class="px-4 py-2 text-gray-500">기본급</td><td class="px-4 py-2 text-right">${numberFormat(data.base_salary)}</td></tr>`;
|
|
html += `<tr class="border-t"><td class="px-4 py-2 text-gray-500">고정연장근로수당</td><td class="px-4 py-2 text-right">${numberFormat(data.overtime_pay)}</td></tr>`;
|
|
html += `<tr class="border-t"><td class="px-4 py-2 text-gray-500">식대(비과세)</td><td class="px-4 py-2 text-right">${numberFormat(data.bonus)}</td></tr>`;
|
|
|
|
if (data.allowances && data.allowances.length > 0) {
|
|
data.allowances.forEach(a => {
|
|
html += `<tr class="border-t"><td class="px-4 py-2 text-gray-500">${a.name}</td><td class="px-4 py-2 text-right">${numberFormat(a.amount)}</td></tr>`;
|
|
});
|
|
}
|
|
|
|
html += `<tr class="border-t bg-blue-50"><td class="px-4 py-2 font-medium text-blue-800">총 지급액</td><td class="px-4 py-2 text-right font-bold text-blue-700">${numberFormat(data.gross_salary)}</td></tr>`;
|
|
|
|
html += '<tr class="bg-red-50"><th class="px-4 py-2 text-left text-red-800 font-medium" colspan="2">공제 항목</th></tr>';
|
|
html += `<tr class="border-t"><td class="px-4 py-2 text-gray-500">국민연금</td><td class="px-4 py-2 text-right">${numberFormat(data.pension)}</td></tr>`;
|
|
html += `<tr class="border-t"><td class="px-4 py-2 text-gray-500">건강보험</td><td class="px-4 py-2 text-right">${numberFormat(data.health_insurance)}</td></tr>`;
|
|
html += `<tr class="border-t"><td class="px-4 py-2 text-gray-500">장기요양보험</td><td class="px-4 py-2 text-right">${numberFormat(data.long_term_care)}</td></tr>`;
|
|
html += `<tr class="border-t"><td class="px-4 py-2 text-gray-500">고용보험</td><td class="px-4 py-2 text-right">${numberFormat(data.employment_insurance)}</td></tr>`;
|
|
html += `<tr class="border-t"><td class="px-4 py-2 text-gray-500">근로소득세</td><td class="px-4 py-2 text-right">${numberFormat(data.income_tax)}</td></tr>`;
|
|
html += `<tr class="border-t"><td class="px-4 py-2 text-gray-500">지방소득세</td><td class="px-4 py-2 text-right">${numberFormat(data.resident_tax)}</td></tr>`;
|
|
|
|
if (data.deductions && data.deductions.length > 0) {
|
|
data.deductions.forEach(d => {
|
|
html += `<tr class="border-t"><td class="px-4 py-2 text-gray-500">${d.name}</td><td class="px-4 py-2 text-right">${numberFormat(d.amount)}</td></tr>`;
|
|
});
|
|
}
|
|
|
|
html += `<tr class="border-t bg-red-50"><td class="px-4 py-2 font-medium text-red-800">총 공제액</td><td class="px-4 py-2 text-right font-bold text-red-700">${numberFormat(data.total_deductions)}</td></tr>`;
|
|
html += `<tr class="bg-emerald-50"><td class="px-4 py-2 font-bold text-emerald-800">실수령액</td><td class="px-4 py-2 text-right font-bold text-lg text-emerald-700">${numberFormat(data.net_salary)}</td></tr>`;
|
|
html += '</table></div>';
|
|
|
|
if (data.confirmed_at) html += `<div class="text-xs text-gray-400">확정일: ${data.confirmed_at}</div>`;
|
|
if (data.paid_at) html += `<div class="text-xs text-gray-400">지급일: ${data.paid_at}</div>`;
|
|
if (data.note) html += `<div class="text-sm text-gray-500 mt-2 p-2 bg-gray-50 rounded">${data.note}</div>`;
|
|
|
|
html += '</div>';
|
|
|
|
document.getElementById('payrollDetailContent').innerHTML = html;
|
|
document.getElementById('payrollDetailModal').classList.remove('hidden');
|
|
}
|
|
|
|
function closePayrollDetailModal() {
|
|
document.getElementById('payrollDetailModal').classList.add('hidden');
|
|
}
|
|
|
|
// ===== 급여 설정 저장 =====
|
|
function savePayrollSettings() {
|
|
const form = document.getElementById('payrollSettingsForm');
|
|
const formData = new FormData(form);
|
|
const data = {};
|
|
const moneyFields = new Set();
|
|
form.querySelectorAll('.money-input').forEach(el => moneyFields.add(el.name));
|
|
for (const [key, value] of formData.entries()) {
|
|
data[key] = moneyFields.has(key) ? parseFloat(value.replace(/,/g, '')) || 0 : value;
|
|
}
|
|
data.auto_calculate = form.querySelector('[name="auto_calculate"]').checked;
|
|
|
|
fetch('{{ route("api.admin.hr.payroll-settings.update") }}', {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
|
'Accept': 'application/json',
|
|
},
|
|
body: JSON.stringify(data),
|
|
})
|
|
.then(r => r.json())
|
|
.then(result => {
|
|
if (result.success) {
|
|
showToast(result.message, 'success');
|
|
} else {
|
|
showToast(result.message || '설정 저장 실패', 'error');
|
|
}
|
|
})
|
|
.catch(err => {
|
|
console.error(err);
|
|
showToast('설정 저장 중 오류가 발생했습니다.', 'error');
|
|
});
|
|
}
|
|
|
|
// ===== 토스트 메시지 =====
|
|
function showToast(message, type) {
|
|
if (typeof window.showToastMessage === 'function') {
|
|
window.showToastMessage(message, type);
|
|
return;
|
|
}
|
|
const color = type === 'success' ? 'bg-emerald-600' : 'bg-red-600';
|
|
const toast = document.createElement('div');
|
|
toast.className = `fixed top-4 right-4 z-[60] ${color} text-white px-4 py-3 rounded-lg shadow-lg text-sm transition-opacity duration-300`;
|
|
toast.textContent = message;
|
|
document.body.appendChild(toast);
|
|
setTimeout(() => { toast.style.opacity = '0'; setTimeout(() => toast.remove(), 300); }, 3000);
|
|
}
|
|
</script>
|
|
@endpush
|