403 lines
12 KiB
PHP

@extends('partials.main')
@section('custom_css')
<style>
#resultList {
max-height: 420px;
overflow-y: auto;
}
.pitstop-type-sticky {
position: sticky;
top: 16px;
z-index: 2;
}
#resultList .list-group-item-action:hover {
background-color: var(--bs-gray-100);
}
/* Catatan: styling <option> tergantung browser, tapi ini membantu di banyak kasus */
#step option[data-locked='1'] {
color: var(--bs-success) !important;
-webkit-text-fill-color: var(--bs-success);
}
</style>
@endsection
@section('content')
<div class="card card-flush">
<div class="card-header pt-7">
<div class="card-title">
<h2 class="mb-0 fw-bold">PitStop Pra Akreditasi</h2>
</div>
<div class="card-toolbar">
<span class="text-muted">Cari karyawan untuk input status per step</span>
</div>
</div>
<div class="card-body pt-0">
<div class="row g-4 align-items-end">
<div class="col-12 col-md-4 align-self-start">
<div class="pitstop-type-sticky">
<label class="form-label fw-semibold mb-2">Tipe Karyawan</label>
<div class="d-flex flex-wrap gap-6">
<label class="form-check form-check-sm form-check-custom form-check-solid mb-0">
<input class="form-check-input" type="radio" name="karyawan_type" id="karyawanTypeInternal" value="internal" checked />
<span class="form-check-label fw-semibold">Karyawan Internal</span>
</label>
<label class="form-check form-check-sm form-check-custom form-check-solid mb-0">
<input class="form-check-input" type="radio" name="karyawan_type" id="karyawanTypeLuar" value="luar" />
<span class="form-check-label fw-semibold">Karyawan Eksternal</span>
</label>
</div>
<div class="form-text">Mode pencarian akan menyesuaikan tipe karyawan.</div>
</div>
</div>
<div class="col-12 col-md-8">
<label for="searchKaryawan" class="form-label fw-semibold">Cari Karyawan</label>
<input
type="text"
id="searchKaryawan"
class="form-control"
placeholder="Ketik nama / NIP..."
autocomplete="off"
aria-label="Cari karyawan"
/>
<ul class="list-group mt-3" id="resultList"></ul>
</div>
</div>
</div>
</div>
<div class="modal fade" id="modalPitstop" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<form id="formPitstop">
<div class="modal-header">
<h5 class="modal-title">Input PitStop</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<input type="hidden" id="karyawan_id" name="karyawan_id" />
<div class="mb-4">
<label for="nama_karyawan" class="form-label">Nama Karyawan</label>
<input type="text" id="nama_karyawan" class="form-control" readonly />
</div>
<div class="mb-4">
<label for="nip" class="form-label" id="label_nip">NIP</label>
<input type="text" id="nip" class="form-control" readonly />
</div>
<div class="notice d-flex bg-light-warning rounded border-warning border border-dashed p-4 mb-5">
<div class="d-flex flex-stack flex-grow-1">
<div class="fw-semibold">
<div class="text-gray-900 fw-bold">Info</div>
<div class="text-gray-700">Data pitstop yang telah diinput tidak akan ditampilkan kembali.</div>
</div>
</div>
</div>
<div class="mb-4">
<label for="step" class="form-label">Step</label>
<select id="step" name="step" class="form-select" required>
@forelse ($masterPitStop as $ps)
<option value="{{ $ps->id }}">({{ $ps->id }}) {{ $ps->nama }}</option>
@empty
<option value="" disabled selected>Tidak ada data</option>
@endforelse
</select>
<div class="form-text" id="stepHint"></div>
</div>
<div class="mb-4">
<label for="nilai" class="form-label" id="label_nilai">Nilai (0-100)</label>
<input
type="number"
id="nilai"
name="nilai"
class="form-control"
min="0"
max="100"
step="1"
inputmode="numeric"
autocomplete="off"
required
/>
</div>
{{-- <div class="mb-2">
<label class="form-label d-block">Status</label>
<div class="d-flex flex-wrap gap-6">
<label class="form-check form-check-sm form-check-custom form-check-solid">
<input class="form-check-input" type="radio" name="status" value="lulus" required />
<span class="form-check-label">Lulus</span>
</label>
<label class="form-check form-check-sm form-check-custom form-check-solid">
<input class="form-check-input" type="radio" name="status" value="tidak_lulus" required />
<span class="form-check-label">Tidak Lulus</span>
</label>
</div>
</div> --}}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Batal</button>
<button type="submit" class="btn btn-primary">
<span class="indicator-label">Simpan</span>
</button>
</div>
</form>
</div>
</div>
</div>
@endsection
@section('custom_js')
<script>
$(document).ready(function () {
let searchTimer = null;
let activeRequest = null;
const $search = $('#searchKaryawan');
const $resultList = $('#resultList');
const $form = $('#formPitstop');
const $step = $('#step');
const $stepHint = $('#stepHint');
const csrf = $('meta[name="csrf-token"]').attr('content');
const escapeHtml = (str) => String(str ?? '').replace(/[&<>"']/g, (m) => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;',
}[m]));
const renderMessage = (message) => {
$resultList.html(`<li class="list-group-item text-muted">${message}</li>`);
};
const clearResults = () => {
$resultList.empty();
};
const getKaryawanType = () => $('input[name="karyawan_type"]:checked').val() || 'internal';
const getListEndpoint = () => (getKaryawanType() === 'luar' ? '/list-karyawan-luar' : '/list-karyawan');
const syncPlaceholder = () => {
const type = getKaryawanType();
$search.attr('placeholder', type === 'luar' ? 'Ketik nama / NIK...' : 'Ketik nama / NIP...');
};
const searchKaryawan = (keyword) => {
if (activeRequest) activeRequest.abort();
renderMessage('Mencari...');
const endpoint = getListEndpoint();
const params = getKaryawanType() === 'luar'
? { search: keyword, limit: 50 }
: { search: keyword, limit: 50 };
activeRequest = $.get(endpoint, params)
.done(function (res) {
const data = res?.data ?? [];
if (!data.length) {
renderMessage('Data tidak ditemukan.');
return;
}
const html = data
.map((item) => {
const nama = item?.namalengkap ?? '-';
const nip = item?.nip_pns ?? '-';
const unit = item?.unit_name ?? '-';
return `
<li class="list-group-item list-group-item-action pilihKaryawan"
role="button"
data-id="${item.id}"
data-nama="${escapeHtml(nama)}"
data-nip="${escapeHtml(nip)}"
data-unit="${escapeHtml(unit)}"
>
<div class="d-flex align-items-center justify-content-between gap-3">
<div class="d-flex flex-column">
<span class="fw-semibold">${escapeHtml(nama)}</span>
<span class="text-muted fs-7">${escapeHtml(nip)}</span>
</div>
<span class="badge badge-light-primary">${escapeHtml(unit)}</span>
</div>
</li>`;
})
.join('');
$resultList.html(html);
})
.fail(function (xhr, status) {
if (status === 'abort') return;
renderMessage('Gagal mengambil data.');
});
};
const lockExistingSteps = (pegawaiId) => {
$stepHint.html('');
// simpan semua opsi awal, supaya bisa di-restore setiap ganti pegawai
if (!$step.data('allOptionsHtml')) {
$step.data('allOptionsHtml', $step.html());
} else {
$step.html($step.data('allOptionsHtml'));
}
$form.find('button[type="submit"]').prop('disabled', false);
const stepsEndpoint = getKaryawanType() === 'luar' ? '/pitstop/pegawai-steps-external' : '/pitstop/pegawai-steps';
return $.get(stepsEndpoint, { pegawai_id: pegawaiId })
.done(function (res) {
const lockedSteps = res?.data?.locked_steps ?? [];
const locked = new Set(lockedSteps.map((v) => String(v)));
// hanya tampilkan step yang belum lulus (yang locked dihapus dari list)
$step.find('option').each(function () {
const val = String($(this).val());
if (locked.has(val)) {
$(this).remove();
}
});
const $first = $step.find('option').first();
if ($first.length) $step.val($first.val());
const availableCount = $step.find('option').length;
if (!availableCount) {
$form.find('button[type="submit"]').prop('disabled', true);
$stepHint.html('<span class="text-success fw-semibold">Semua step sudah selesai dikerjakan</span>');
}
})
.fail(function () {
// kalau gagal ambil data lock, tetap biarkan user submit (server akan validasi)
});
};
$step.on('change', function () {
// opsi yang tampil sudah pasti belum lulus
$stepHint.html('');
});
$search.on('keyup', function () {
const keyword = String($(this).val() ?? '').trim();
if (keyword.length < 2) {
if (activeRequest) activeRequest.abort();
clearResults();
return;
}
clearTimeout(searchTimer);
searchTimer = setTimeout(() => searchKaryawan(keyword), 250);
});
$(document).on('change', 'input[name="karyawan_type"]', function () {
if (activeRequest) activeRequest.abort();
$search.val('');
syncPlaceholder();
clearResults();
});
syncPlaceholder();
$(document).on('click', '.pilihKaryawan', function () {
$form.trigger('reset');
$('#nama_karyawan').val($(this).data('nama'));
$('#nip').val($(this).data('nip'));
$('#karyawan_id').val($(this).data('id'));
lockExistingSteps($(this).data('id'));
getKaryawanType() === 'luar' ? $('#label_nip').text('NIK') : $('#label_nip').text('NIP');
new bootstrap.Modal(document.getElementById('modalPitstop')).show();
});
$form.on('submit', function (e) {
e.preventDefault();
if ($step.find('option:selected').attr('data-locked') === '1') {
Swal.fire({
toast: true,
position: 'top-end',
icon: 'info',
title: 'Step ini sudah lulus dan terkunci.',
showConfirmButton: false,
timer: 2200,
timerProgressBar: true,
});
return;
}
const rawNilai = String($('#nilai').val() ?? '').trim();
const nilai = rawNilai === '' ? NaN : Number(rawNilai);
if (!Number.isFinite(nilai) || nilai < 0 || nilai > 100) {
Swal.fire({
toast: true,
position: 'top-end',
icon: 'warning',
title: 'Nilai harus di antara 0 sampai 100.',
showConfirmButton: false,
timer: 2400,
timerProgressBar: true,
});
return;
}
const payload = {
karyawan_id: $('#karyawan_id').val(),
step: $('#step').val(),
// status: $('input[name="status"]:checked').val(),
nilai: nilai,
tipe_karyawan: getKaryawanType(),
};
$.ajax({
url: '/pitstop/submit',
method: 'POST',
headers: { 'X-CSRF-TOKEN': csrf },
data: payload,
})
.done(function () {
const modalEl = document.getElementById('modalPitstop');
const modal = bootstrap.Modal.getInstance(modalEl);
if (modal) modal.hide();
Swal.fire({
toast: true,
position: 'top-end',
icon: 'success',
title: 'Berhasil submit pitstop.',
showConfirmButton: false,
timer: 2000,
timerProgressBar: true,
});
})
.fail(function (xhr) {
const msg =
xhr?.responseJSON?.message ||
(xhr?.status === 422 ? 'Validasi gagal.' : 'Gagal menyimpan data.');
Swal.fire({
toast: true,
position: 'top-end',
icon: 'error',
title: msg,
showConfirmButton: false,
timer: 2600,
timerProgressBar: true,
});
});
});
});
</script>
@endsection