Initial commit - lms-v2 + CLAUDE.md
This commit is contained in:
@@ -0,0 +1,238 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
|
||||
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||
|
||||
<style>
|
||||
/* Penyesuaian UI Select2 agar senada dengan Tailwind dan menyembunyikan elemen asli */
|
||||
.select2-container .select2-selection--single { height: 42px; border-radius: 0.75rem; border-color: #e2e8f0; background-color: #f8fafc; }
|
||||
.select2-container--default .select2-selection--single .select2-selection__rendered { line-height: 42px; font-size: 0.875rem; color: #334155; padding-left: 1rem; }
|
||||
.select2-container--default .select2-selection--single .select2-selection__arrow { height: 40px; right: 10px; }
|
||||
.select2-hidden-accessible { position: absolute !important; width: 1px !important; height: 1px !important; padding: 0 !important; margin: -1px !important; overflow: hidden !important; clip: rect(0,0,0,0) !important; white-space: nowrap !important; border: 0 !important; }
|
||||
</style>
|
||||
|
||||
<div class="max-w-5xl mx-auto px-4 py-8">
|
||||
<div class="mb-6 flex items-center space-x-3">
|
||||
<a href="{{ route('admin.employees.index') }}" class="text-slate-400 hover:text-blue-600 transition-colors">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path></svg>
|
||||
</a>
|
||||
<h2 class="text-2xl font-bold text-slate-800 tracking-tight">Tambah Karyawan Baru</h2>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 overflow-hidden">
|
||||
<div class="bg-blue-50/50 p-4 border-b border-slate-100 flex items-start">
|
||||
<svg class="w-5 h-5 text-blue-600 mt-0.5 mr-3 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||
<p class="text-sm text-blue-800">Lengkapi formulir di bawah ini. Password default: <span class="font-mono font-bold bg-white px-2 py-0.5 rounded border border-blue-200">12345678</span></p>
|
||||
</div>
|
||||
|
||||
<form action="{{ route('admin.employees.store') }}" method="POST" enctype="multipart/form-data" class="p-6 sm:p-8 space-y-6">
|
||||
@csrf
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-slate-700 mb-2">NIK <span class="text-red-500">*</span></label>
|
||||
<input type="text" name="nik" value="{{ old('nik') }}" required class="w-full px-4 py-2.5 bg-slate-50 border border-slate-200 rounded-xl focus:bg-white focus:ring-2 focus:ring-blue-500/20 focus:border-blue-600 transition-all text-sm">
|
||||
@error('nik') <span class="text-xs text-red-500 mt-1 block">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-slate-700 mb-2">No. KTP</label>
|
||||
<input type="text" name="identity_number" value="{{ old('identity_number') }}" class="w-full px-4 py-2.5 bg-slate-50 border border-slate-200 rounded-xl focus:bg-white focus:ring-2 focus:ring-blue-500/20 focus:border-blue-600 transition-all text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-slate-700 mb-2">Inisial <span class="text-blue-500 font-normal text-xs">(Auto Suggest)</span></label>
|
||||
<input type="text" name="initial" id="initial" value="{{ old('initial') }}" maxlength="3" class="w-full px-4 py-2.5 bg-slate-50 border border-slate-200 rounded-xl focus:bg-white focus:ring-2 focus:ring-blue-500/20 focus:border-blue-600 transition-all text-sm uppercase">
|
||||
</div>
|
||||
|
||||
<div class="lg:col-span-2">
|
||||
<label class="block text-sm font-bold text-slate-700 mb-2">Nama Depan <span class="text-red-500">*</span></label>
|
||||
<input type="text" name="first_name" id="first_name" value="{{ old('first_name') }}" required class="w-full px-4 py-2.5 bg-slate-50 border border-slate-200 rounded-xl focus:bg-white focus:ring-2 focus:ring-blue-500/20 focus:border-blue-600 transition-all text-sm">
|
||||
@error('first_name') <span class="text-xs text-red-500 mt-1 block">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-slate-700 mb-2">Nama Belakang</label>
|
||||
<input type="text" name="last_name" id="last_name" value="{{ old('last_name') }}" class="w-full px-4 py-2.5 bg-slate-50 border border-slate-200 rounded-xl focus:bg-white focus:ring-2 focus:ring-blue-500/20 focus:border-blue-600 transition-all text-sm">
|
||||
</div>
|
||||
|
||||
<div class="lg:col-span-2">
|
||||
<label class="block text-sm font-bold text-slate-700 mb-2">Email Perusahaan <span class="text-red-500">*</span></label>
|
||||
<input type="email" name="email" value="{{ old('email') }}" required class="w-full px-4 py-2.5 bg-slate-50 border border-slate-200 rounded-xl focus:bg-white focus:ring-2 focus:ring-blue-500/20 focus:border-blue-600 transition-all text-sm">
|
||||
@error('email') <span class="text-xs text-red-500 mt-1 block">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-slate-700 mb-2">No. HP / WA</label>
|
||||
<input type="text" name="phone" value="{{ old('phone') }}" class="w-full px-4 py-2.5 bg-slate-50 border border-slate-200 rounded-xl focus:bg-white focus:ring-2 focus:ring-blue-500/20 focus:border-blue-600 transition-all text-sm">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-slate-700 mb-2">Jenis Kelamin <span class="text-red-500">*</span></label>
|
||||
<select name="gender" class="select2 w-full text-sm" required>
|
||||
<option value="">-- Pilih --</option>
|
||||
<option value="L" {{ old('gender') == 'L' ? 'selected' : '' }}>Laki-laki</option>
|
||||
<option value="P" {{ old('gender') == 'P' ? 'selected' : '' }}>Perempuan</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-slate-700 mb-2">Tanggal Lahir</label>
|
||||
<input type="date" name="date_of_birth" value="{{ old('date_of_birth') }}" class="w-full px-4 py-2.5 bg-slate-50 border border-slate-200 rounded-xl focus:bg-white focus:ring-2 focus:ring-blue-500/20 focus:border-blue-600 transition-all text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-slate-700 mb-2">Tanggal Bergabung</label>
|
||||
<input type="date" name="join_date" value="{{ old('join_date') }}" class="w-full px-4 py-2.5 bg-slate-50 border border-slate-200 rounded-xl focus:bg-white focus:ring-2 focus:ring-blue-500/20 focus:border-blue-600 transition-all text-sm">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-slate-700 mb-2">Departemen <span class="text-red-500">*</span></label>
|
||||
<select name="department_id" id="deptSelect" class="select2 w-full text-sm" required>
|
||||
<option value="">-- Pilih Departemen --</option>
|
||||
@foreach($departments as $dept)
|
||||
<option value="{{ $dept->id }}" {{ old('department_id') == $dept->id ? 'selected' : '' }}>{{ $dept->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-slate-700 mb-2">Posisi / Jabatan <span class="text-red-500">*</span></label>
|
||||
<select name="position_id" id="posSelect" class="select2 w-full text-sm" required>
|
||||
<option value="">-- Pilih Dept Terlebih Dahulu --</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="bg-slate-50 p-4 border border-slate-200 rounded-xl">
|
||||
<label class="block text-sm font-bold text-slate-700 mb-2">Hak Akses Sistem Utama <span class="text-red-500">*</span></label>
|
||||
<select name="role" class="select2 w-full text-sm" required>
|
||||
<option value="karyawan" {{ old('role') == 'karyawan' ? 'selected' : '' }}>Trainee (Karyawan)</option>
|
||||
@if(auth()->user()->hasRole('superadmin'))
|
||||
<option value="admin" {{ old('role') == 'admin' ? 'selected' : '' }}>Administrator</option>
|
||||
@endif
|
||||
</select>
|
||||
|
||||
<label class="flex items-center space-x-2 cursor-pointer mt-4">
|
||||
<input type="checkbox" name="is_trainer" value="1" {{ old('is_trainer') ? 'checked' : '' }} class="w-4 h-4 text-blue-600 border-slate-300 rounded focus:ring-blue-500">
|
||||
<span class="text-sm font-semibold text-slate-700">Jadikan Trainer / Pemateri juga</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-slate-100 pt-6 mt-6">
|
||||
<label class="block text-sm font-bold text-slate-700 mb-2">Modul SOP Training (Marketing)</label>
|
||||
<select name="training_matrix_id" class="select2 w-full text-sm">
|
||||
<option value="">-- Kosongkan jika bukan Plant Dept --</option>
|
||||
@foreach($trainingMatrices as $matrix)
|
||||
<option value="{{ $matrix->id }}" {{ old('training_matrix_id') == $matrix->id ? 'selected' : '' }}>{{ $matrix->title ?? $matrix->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-slate-100 pt-6 mt-6" x-data="{ documents: [{ id: 1 }], addDocument() { this.documents.push({ id: Date.now() }); }, removeDocument(id) { this.documents = this.documents.filter(doc => doc.id !== id); } }">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-slate-700">Dokumen Karyawan</label>
|
||||
<p class="text-xs text-slate-500 mt-1">CV, KTP, Ijazah (Otomatis terunggah ke Nextcloud)</p>
|
||||
</div>
|
||||
<button type="button" @click="addDocument()" class="text-xs bg-blue-50 text-blue-700 font-bold px-3 py-1.5 rounded-lg hover:bg-blue-100">
|
||||
+ Tambah Kolom File
|
||||
</button>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<template x-for="(doc, index) in documents" :key="doc.id">
|
||||
<div class="flex items-center gap-3 bg-slate-50 p-3 rounded-lg border border-slate-200">
|
||||
<span class="text-sm font-bold text-slate-400 w-6" x-text="index + 1 + '.'"></span>
|
||||
<input type="file" name="documents[]" class="flex-1 text-sm file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100 cursor-pointer">
|
||||
<button type="button" x-show="documents.length > 1" @click="removeDocument(doc.id)" class="text-red-500 hover:text-red-700 p-2">
|
||||
Hapus
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-6 flex justify-end space-x-3 border-t border-slate-100 mt-8">
|
||||
<a href="{{ route('admin.employees.index') }}" class="px-5 py-2.5 text-sm font-semibold text-slate-600 hover:bg-slate-100 rounded-xl transition-colors">Batal</a>
|
||||
<button type="submit" class="px-5 py-2.5 text-sm font-semibold text-white bg-blue-600 hover:bg-blue-700 rounded-xl shadow-md transition-all">Simpan Karyawan</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// 1. Inisialisasi Select2 di semua dropdown yang ada class select2
|
||||
$('.select2').select2({ width: '100%' });
|
||||
|
||||
// 2. Logika Dropdown Bertingkat (Dept -> Posisi)
|
||||
const mapping = @json($deptPosMapping ?? []);
|
||||
const posSelect = $('#posSelect');
|
||||
const deptSelect = $('#deptSelect');
|
||||
const oldPos = "{{ old('position_id') }}";
|
||||
|
||||
function updatePositionDropdown() {
|
||||
let deptId = deptSelect.val();
|
||||
posSelect.empty();
|
||||
posSelect.append(new Option('-- Pilih Jabatan --', ''));
|
||||
|
||||
if (deptId && mapping[deptId]) {
|
||||
mapping[deptId].forEach(function(pos) {
|
||||
let selected = (pos.id == oldPos);
|
||||
posSelect.append(new Option(pos.name, pos.id, false, selected));
|
||||
});
|
||||
} else {
|
||||
// Munculkan semua jika Dept belum terpilih
|
||||
@foreach($positions as $p)
|
||||
posSelect.append(new Option("{{ $p->name }}", "{{ $p->id }}", false, ("{{ $p->id }}" == oldPos)));
|
||||
@endforeach
|
||||
}
|
||||
posSelect.trigger('change.select2');
|
||||
}
|
||||
|
||||
deptSelect.on('change', updatePositionDropdown);
|
||||
updatePositionDropdown(); // Eksekusi saat load pertama kali
|
||||
});
|
||||
|
||||
// 3. FITUR AUTO-SUGGEST INITIAL 3 DIGIT
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
const existingInitials = @json($existingInitials ?? []);
|
||||
const firstNameInput = document.getElementById('first_name');
|
||||
const lastNameInput = document.getElementById('last_name');
|
||||
const initialInput = document.getElementById('initial');
|
||||
|
||||
function generateInitial() {
|
||||
let first = firstNameInput.value.trim().toLowerCase();
|
||||
let last = lastNameInput.value.trim().toLowerCase();
|
||||
let fullName = (first + ' ' + last).trim();
|
||||
|
||||
if (fullName.length === 0) return;
|
||||
|
||||
let words = fullName.split(/\s+/).filter(w => w.length > 0);
|
||||
let initial = '';
|
||||
|
||||
// Algoritma: 3 kata ambil huruf pertama, 2 kata ambil 1 dpn & 2 blkg, 1 kata ambil 3 awal.
|
||||
if (words.length >= 3) {
|
||||
initial = words[0][0] + words[1][0] + words[2][0];
|
||||
} else if (words.length === 2) {
|
||||
initial = words[0][0] + words[1].substring(0, 2);
|
||||
} else if (words.length === 1) {
|
||||
initial = words[0].substring(0, 3);
|
||||
}
|
||||
|
||||
initial = initial.replace(/[^a-z]/g, '');
|
||||
while(initial.length < 3) { initial += 'x'; }
|
||||
initial = initial.substring(0, 3);
|
||||
|
||||
let baseInitial = initial.substring(0, 2);
|
||||
let counter = 0;
|
||||
let alphabet = 'abcdefghijklmnopqrstuvwxyz';
|
||||
|
||||
// Cek ke DB: jika inisial sudah ada, ganti huruf ke-3 secara alphabet.
|
||||
while (existingInitials.includes(initial) && counter < 26) {
|
||||
initial = baseInitial + alphabet[counter];
|
||||
counter++;
|
||||
}
|
||||
initialInput.value = initial.toUpperCase();
|
||||
}
|
||||
|
||||
firstNameInput.addEventListener('input', generateInitial);
|
||||
lastNameInput.addEventListener('input', generateInitial);
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
Reference in New Issue
Block a user