sedikit penyesuaian

This commit is contained in:
jouel88 2026-04-07 10:46:34 +07:00
parent 96bf631b45
commit b2c5d494ab
61 changed files with 1009 additions and 673 deletions

View File

@ -99,23 +99,24 @@ public function verifyFace(Request $request)
'tipe' => 'nullable|string|in:presensi,test',
]);
$tipe = $request->input('tipe', 'presensi');
try {
set_time_limit(0);
$user = $request->user();
$tipe = $request->input('tipe', 'presensi');
$file = $request->file('foto');
$result = $this->faceService->verifyFace($user->id, $file);
\DB::table('log_verifikasi_wajah')->insert([
'id_user' => $user->id,
'confidence' => $result['confidence'] ?? null,
'svm_confidence' => $result['svm_confidence'] ?? null,
'normalized_distance' => $result['normalized_distance'] ?? null,
'verification_status' => $result['verification_status'] ?? null,
'is_match' => $result['verified'] ?? false,
'blur_score' => $result['blur_score'] ?? null,
'skor_kepercayaan' => $result['confidence'] ?? null,
'skor_svm' => $result['svm_confidence'] ?? null,
'jarak_normalisasi' => $result['normalized_distance'] ?? null,
'status_verifikasi' => $result['verification_status'] ?? null,
'apakah_cocok' => $result['verified'] ?? false,
'skor_blur' => $result['blur_score'] ?? null,
'tipe' => $tipe,
'created_at' => now(),
'updated_at' => now(),
@ -131,9 +132,9 @@ public function verifyFace(Request $request)
if (isset($user)) {
\DB::table('log_verifikasi_wajah')->insert([
'id_user' => $user->id,
'confidence' => 0,
'verification_status' => 'ERROR',
'is_match' => false,
'skor_kepercayaan' => 0,
'status_verifikasi' => 'ERROR',
'apakah_cocok' => false,
'tipe' => $tipe ?? 'presensi',
'created_at' => now(),
'updated_at' => now(),

View File

@ -2,26 +2,34 @@
namespace App\Http\Controllers\Api;
use App\Enums\JenisPengurangan;
use App\Enums\StatusPengajuan;
use App\Helpers\ApiResponse;
use App\Http\Controllers\Controller;
use App\Models\Poin;
use App\Models\PenggunaanPoin;
use App\Services\NotifikasiService;
use App\Services\PoinService;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator;
class PoinController extends Controller
{
protected $poinService;
public function __construct(PoinService $poinService)
{
$this->poinService = $poinService;
}
public function getExpiringPoints()
{
try {
$userId = auth()->id();
$userId = Auth::id();
$poinService = new PoinService();
$poinExpiring = $poinService->getExpiringPoints($userId);
$activePoints = $poinService->getActivePoints($userId);
$poinExpiring = $this->poinService->getExpiringPoints($userId);
$activePoints = $this->poinService->getActivePoints($userId);
$firstExpiring = $poinExpiring->first();
$formattedExpiring = null;
@ -46,10 +54,8 @@ public function getExpiringPoints()
public function getPointHistory()
{
try {
$userId = auth()->id();
$poinService = new PoinService();
$history = $poinService->getPointHistory($userId);
$userId = Auth::id();
$history = $this->poinService->getPointHistory($userId);
return ApiResponse::success(['history' => $history]);
@ -62,10 +68,10 @@ public function redeem(Request $request)
{
$validator = Validator::make($request->all(), [
'jumlah' => 'required|integer|min:1',
'keterangan' => 'required|string',
'keterangan' => 'required|string|max:500',
'id_pengurangan' => 'required|integer',
'jam_masuk_custom' => 'required_if:id_pengurangan,4|date_format:H:i|nullable',
'jam_pulang_custom' => 'required_if:id_pengurangan,5|date_format:H:i|nullable',
'jam_masuk_custom' => 'required_if:id_pengurangan,' . JenisPengurangan::MASUK_SIANG_POIN . '|date_format:H:i|nullable',
'jam_pulang_custom' => 'required_if:id_pengurangan,' . JenisPengurangan::PULANG_CEPAT_POIN . '|date_format:H:i|nullable',
]);
if ($validator->fails()) {
@ -73,18 +79,16 @@ public function redeem(Request $request)
}
try {
$userId = auth()->id();
$userId = Auth::id();
$jumlah = $request->jumlah;
$keterangan = $request->keterangan;
$poinService = new PoinService();
$totalPoin = $poinService->getActivePoints($userId);
$totalPoin = $this->poinService->getActivePoints($userId);
if ($totalPoin < $jumlah) {
return ApiResponse::error('Saldo poin tidak mencukupi', 400);
return ApiResponse::error('Saldo poin tidak mencukupi. Saldo aktif: ' . $totalPoin . ', dibutuhkan: ' . $jumlah, 400);
}
preg_match('/\[(\d{4}-\d{2}-\d{2})\]/', $keterangan, $matches);
preg_match('/\[(\d{4}-\d{2}-\d{2})\]/', $request->keterangan, $matches);
$tanggalPenggunaan = isset($matches[1]) ? $matches[1] : now()->format('Y-m-d');
$jadwal = \App\Models\JadwalKerja::with('shift')
@ -113,6 +117,14 @@ public function redeem(Request $request)
'tanggal_diajukan' => now(),
]);
$user = Auth::user();
app(NotifikasiService::class)->kirimKeRole(
'hrd',
'pengajuan_baru',
'Pengajuan Penggunaan Poin',
$user->nama_lengkap . ' mengajukan penggunaan ' . $jumlah . ' poin.'
);
return ApiResponse::success(null, 'Pengajuan penukaran poin berhasil disimpan');
} catch (\Exception $e) {
return ApiResponse::error($e->getMessage(), 400);

View File

@ -258,8 +258,15 @@ public function history(Request $request)
try {
$masuk = Carbon::parse($item->tanggal . ' ' . $item->jam_masuk);
$pulang = Carbon::parse($item->tanggal . ' ' . $item->jam_pulang);
$diff = $masuk->diff($pulang);
$totalJam = $diff->h . 'j ' . $diff->i . 'm';
if ($pulang->lessThan($masuk)) {
$pulang->addDay();
}
$diffMinutes = $pulang->diffInMinutes($masuk);
$hours = floor($diffMinutes / 60);
$minutes = $diffMinutes % 60;
$totalJam = $hours . 'j ' . $minutes . 'm';
} catch (\Exception $e) {
$totalJam = '-';
}

View File

@ -3,6 +3,7 @@
namespace App\Http\Controllers;
use App\Models\HariLibur;
use App\Http\Requests\StoreHariLiburRequest;
use Illuminate\Http\Request;
use Carbon\Carbon;
@ -27,15 +28,9 @@ public function index(Request $request)
return view('hari_libur.index', compact('hariLiburs', 'kantors'));
}
public function store(Request $request)
public function store(StoreHariLiburRequest $request)
{
$request->validate([
'tanggal' => 'required|date',
'keterangan' => 'required|string|max:255',
'id_kantor' => 'nullable|exists:kantor,id_kantor',
]);
HariLibur::create($request->all());
HariLibur::create($request->validated());
return redirect()->route('hari-libur.index')
->with('success', 'Hari libur berhasil ditambahkan.');
@ -44,13 +39,13 @@ public function store(Request $request)
public function update(Request $request, $id)
{
$request->validate([
'tanggal' => 'required|date',
'keterangan' => 'required|string|max:255',
'id_kantor' => 'nullable|exists:kantor,id_kantor',
'tanggal' => 'required|date',
'keterangan' => 'required|string|max:255',
'id_kantor' => 'nullable|exists:kantor,id_kantor',
]);
$hariLibur = HariLibur::where('id', '=', $id)->first() ?? HariLibur::where('id_hari_libur', '=', $id)->firstOrFail();
$hariLibur->update($request->all());
$hariLibur = HariLibur::findOrFail($id);
$hariLibur->update($request->only(['tanggal', 'keterangan', 'id_kantor']));
return redirect()->route('hari-libur.index')
->with('success', 'Hari libur berhasil diperbarui.');
@ -58,8 +53,7 @@ public function update(Request $request, $id)
public function destroy($id)
{
$hariLibur = HariLibur::where('id', '=', $id)->first() ?? HariLibur::where('id_hari_libur', '=', $id)->firstOrFail();
$hariLibur->delete();
HariLibur::findOrFail($id)->delete();
return redirect()->route('hari-libur.index')
->with('success', 'Hari libur berhasil dihapus.');

View File

@ -13,6 +13,8 @@
use App\Models\PenggunaanPoin;
use App\Services\PoinService;
use Illuminate\Support\Facades\Auth;
use App\Http\Requests\StoreJadwalRequest;
use App\Http\Requests\BulkDestroyJadwalRequest;
class JadwalController extends Controller
{
@ -28,7 +30,7 @@ public function index()
return $s;
});
$pegawaiQuery = User::where('status_aktif', 1);
$pegawaiQuery = User::where('status_aktif', 1)->bukanHrd();
if (!$isGlobalAdmin) {
$pegawaiQuery->where('id_kantor', $user->id_kantor);
}
@ -211,7 +213,7 @@ public function create()
$kantor = $isGlobalAdmin ? Kantor::all() : Kantor::where('id_kantor', $user->id_kantor)->get();
$divisi = Divisi::all();
$pegawaiQuery = User::where('status_aktif', 1)->with(['kantor', 'jabatan']);
$pegawaiQuery = User::where('status_aktif', 1)->bukanHrd()->with(['kantor', 'jabatan']);
if (!$isGlobalAdmin) {
$pegawaiQuery->where('id_kantor', $user->id_kantor);
}
@ -220,15 +222,9 @@ public function create()
return view('jadwal.create', compact('shifts', 'pegawai', 'kantor', 'divisi'));
}
public function store(Request $request)
public function store(StoreJadwalRequest $request)
{
$request->validate([
'tanggal_mulai' => 'required|date',
'tanggal_selesai' => 'required|date|after_or_equal:tanggal_mulai',
'id_shift' => 'required|exists:shift_kerja,id_shift',
'user_ids' => 'required|array',
'user_ids.*' => 'exists:users,id',
]);
$data = $request->validated();
// Guard: pastikan ada data shift sebelum memproses
if (ShiftKerja::count() === 0) {
@ -249,12 +245,12 @@ public function store(Request $request)
}
}
$startDate = Carbon::parse($request->tanggal_mulai);
$endDate = Carbon::parse($request->tanggal_selesai);
$startDate = Carbon::parse($data['tanggal_mulai']);
$endDate = Carbon::parse($data['tanggal_selesai']);
$period = \Carbon\CarbonPeriod::create($startDate, $endDate);
$shiftId = $request->id_shift;
$userIds = $request->user_ids;
$shiftId = $data['id_shift'];
$userIds = $data['user_ids'];
$count = 0;
$refundCount = 0;
@ -364,38 +360,33 @@ public function destroy($id)
return response()->json(['success' => true, 'message' => 'Jadwal berhasil dihapus']);
}
public function bulkDestroy(Request $request)
public function bulkDestroy(BulkDestroyJadwalRequest $request)
{
$request->validate([
'tanggal_mulai' => 'required|date',
'tanggal_selesai' => 'required|date|after_or_equal:tanggal_mulai',
'user_ids' => 'required|array',
'user_ids.*' => 'exists:users,id',
]);
$data = $request->validated();
$user = Auth::user();
$isGlobalAdmin = $user->isGlobalAdmin();
if (!$isGlobalAdmin) {
$invalidUsers = User::whereIn('id', $request->user_ids)
$invalidUsers = User::whereIn('id', $data['user_ids'])
->where('id_kantor', '!=', $user->id_kantor)
->exists();
if ($invalidUsers) {
return redirect()->back()->with('error', 'Anda tidak diizinkan menghapus jadwal karyawan di luar kantor Anda.');
return redirect()->route('jadwal.index')->with('error', 'Anda tidak diizinkan menghapus jadwal karyawan di luar kantor Anda.');
}
}
try {
DB::transaction(function () use ($request) {
JadwalKerja::whereIn('id_user', $request->user_ids)
->whereBetween('tanggal', [$request->tanggal_mulai, $request->tanggal_selesai])
DB::transaction(function () use ($data) {
JadwalKerja::whereIn('id_user', $data['user_ids'])
->whereBetween('tanggal', [$data['tanggal_mulai'], $data['tanggal_selesai']])
->delete();
});
return redirect()->back()->with('success', 'Jadwal kerja berhasil dihapus pada rentang tanggal tersebut.');
return redirect()->route('jadwal.index')->with('success', 'Jadwal kerja berhasil dihapus pada rentang tanggal tersebut.');
} catch (\Exception $e) {
return redirect()->back()->with('error', 'Terjadi kesalahan saat menghapus jadwal: ' . $e->getMessage());
return redirect()->route('jadwal.index')->with('error', 'Terjadi kesalahan saat menghapus jadwal: ' . $e->getMessage());
}
}

View File

@ -6,6 +6,7 @@
use App\Models\User;
use App\Models\Presensi;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Carbon\Carbon;
use Maatwebsite\Excel\Facades\Excel;
@ -17,11 +18,15 @@ class LaporanController extends Controller
{
public function index(Request $request)
{
$bulan = $request->input('bulan', date('m'));
$tahun = $request->input('tahun', date('Y'));
$bulan = $request->input('bulan', date('m'));
$tahun = $request->input('tahun', date('Y'));
$divisiId = $request->input('id_divisi');
$rekap = $this->buildRekap($bulan, $tahun, $divisiId);
$authUser = Auth::user();
$isGlobalAdmin = $authUser->isGlobalAdmin();
$kantorId = $isGlobalAdmin ? null : $authUser->id_kantor;
$rekap = $this->buildRekap($bulan, $tahun, $divisiId, $kantorId);
$divisiList = \App\Models\Divisi::all();
return view('laporan.index', compact('rekap', 'bulan', 'tahun', 'divisiList', 'divisiId'));
@ -29,15 +34,24 @@ public function index(Request $request)
public function cuti(Request $request)
{
$bulan = $request->input('bulan', date('m'));
$tahun = $request->input('tahun', date('Y'));
$search = $request->input('search');
$bulan = $request->input('bulan', date('m'));
$tahun = $request->input('tahun', date('Y'));
$search = $request->input('search');
$divisiId = $request->input('id_divisi');
$authUser = Auth::user();
$isGlobalAdmin = $authUser->isGlobalAdmin();
$query = \App\Models\PengajuanIzin::with(['user.divisi', 'jenisIzin'])
->whereYear('tanggal_mulai', $tahun)
->whereMonth('tanggal_mulai', $bulan);
if (!$isGlobalAdmin) {
$query->whereHas('user', function ($q) use ($authUser) {
$q->where('id_kantor', $authUser->id_kantor);
});
}
if ($search) {
$query->whereHas('user', function($q) use ($search) {
$q->where('nama_lengkap', 'like', "%{$search}%")
@ -51,7 +65,7 @@ public function cuti(Request $request)
});
}
$izinList = $query->orderBy('tanggal_mulai', 'desc')->paginate(15)->withQueryString();
$izinList = $query->orderBy('tanggal_mulai', 'desc')->paginate(15)->withQueryString();
$divisiList = \App\Models\Divisi::all();
return view('laporan.izin', compact('izinList', 'bulan', 'tahun', 'search', 'divisiId', 'divisiList'));
@ -64,10 +78,19 @@ public function exportIzinExcel(Request $request)
$search = $request->input('search');
$divisiId = $request->input('id_divisi');
$authUser = Auth::user();
$isGlobalAdmin = $authUser->isGlobalAdmin();
$query = \App\Models\PengajuanIzin::with(['user.divisi', 'jenisIzin'])
->whereYear('tanggal_mulai', $tahun)
->whereMonth('tanggal_mulai', $bulan);
if (!$isGlobalAdmin) {
$query->whereHas('user', function ($q) use ($authUser) {
$q->where('id_kantor', $authUser->id_kantor);
});
}
if ($search) {
$query->whereHas('user', function ($q) use ($search) {
$q->where('nama_lengkap', 'like', "%{$search}%")
@ -94,11 +117,14 @@ public function exportIzinExcel(Request $request)
public function exportExcel(Request $request)
{
$bulan = $request->input('bulan', date('m'));
$tahun = $request->input('tahun', date('Y'));
$bulan = $request->input('bulan', date('m'));
$tahun = $request->input('tahun', date('Y'));
$divisiId = $request->input('id_divisi');
$rekap = $this->buildRekap($bulan, $tahun, $divisiId);
$authUser = Auth::user();
$kantorId = $authUser->isGlobalAdmin() ? null : $authUser->id_kantor;
$rekap = $this->buildRekap($bulan, $tahun, $divisiId, $kantorId);
if (empty($rekap)) {
return redirect()->back()->with('error', 'Tidak ada data presensi untuk diekspor pada periode tersebut.');
@ -111,11 +137,14 @@ public function exportExcel(Request $request)
public function exportPdf(Request $request)
{
$bulan = $request->input('bulan', date('m'));
$tahun = $request->input('tahun', date('Y'));
$bulan = $request->input('bulan', date('m'));
$tahun = $request->input('tahun', date('Y'));
$divisiId = $request->input('id_divisi');
$rekap = $this->buildRekap($bulan, $tahun, $divisiId);
$authUser = Auth::user();
$kantorId = $authUser->isGlobalAdmin() ? null : $authUser->id_kantor;
$rekap = $this->buildRekap($bulan, $tahun, $divisiId, $kantorId);
$pdf = Pdf::loadView('laporan.pdf', compact('rekap', 'bulan', 'tahun'));
$pdf->setPaper('a4', 'landscape');
@ -123,19 +152,24 @@ public function exportPdf(Request $request)
return $pdf->download("Laporan_Presensi_{$bulan}_{$tahun}.pdf");
}
private function buildRekap(int $bulan, int $tahun, $divisiId = null): array
private function buildRekap(int $bulan, int $tahun, $divisiId = null, $kantorId = null): array
{
$idTepatWaktu = StatusPresensi::TEPAT_WAKTU;
$idTerlambat = StatusPresensi::TERLAMBAT;
$idIzin = StatusPresensi::IZIN;
$idSakit = StatusPresensi::SAKIT;
$idAlpha = StatusPresensi::ALPHA;
$idTerlambat = StatusPresensi::TERLAMBAT;
$idIzin = StatusPresensi::IZIN;
$idSakit = StatusPresensi::SAKIT;
$idAlpha = StatusPresensi::ALPHA;
$pegawaiQuery = User::with(['jabatan', 'divisi'])
->where('status_aktif', 1)
->bukanHrd()
->select('id', 'nama_lengkap', 'nik', 'id_jabatan', 'id_divisi')
->orderBy('nama_lengkap', 'asc');
if ($kantorId) {
$pegawaiQuery->where('id_kantor', $kantorId);
}
if ($divisiId) {
$pegawaiQuery->where('id_divisi', $divisiId);
}

View File

@ -2,9 +2,11 @@
namespace App\Http\Controllers;
use App\Enums\StatusPengajuan;
use App\Models\User;
use App\Models\Lembur;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Maatwebsite\Excel\Facades\Excel;
@ -15,83 +17,80 @@ class LaporanLemburController extends Controller
public function index(Request $request)
{
$bulan = $request->input('bulan', date('m'));
$tahun = $request->input('tahun', date('Y'));
$search = $request->input('search');
$bulan = $request->input('bulan', date('m'));
$tahun = $request->input('tahun', date('Y'));
$search = $request->input('search');
$divisiId = $request->input('id_divisi');
$query = User::with(['divisi', 'jabatan', 'lemburs' => function($q) use ($bulan, $tahun) {
$q->whereYear('tanggal_lembur', $tahun)
->whereMonth('tanggal_lembur', $bulan)
->where('id_status', 2);
}])
->whereHas('roles', function($q) {
$q->where('nama_role', '!=', 'super_admin');
});
$authUser = Auth::user();
$isGlobalAdmin = $authUser->isGlobalAdmin();
if ($search) {
$query->where(function($q) use ($search) {
$q->where('nama_lengkap', 'like', '%' . $search . '%')
->orWhere('nik', 'like', '%' . $search . '%');
});
}
[$pegawai, $rekap] = $this->buildRekap(
$bulan, $tahun, $divisiId,
$isGlobalAdmin ? null : $authUser->id_kantor,
$search
);
if ($divisiId) {
$query->where('id_divisi', $divisiId);
}
$pegawai = $query->orderBy('nama_lengkap', 'asc')->paginate(15)->withQueryString();
$divisiList = \App\Models\Divisi::all();
$rekap = [];
foreach ($pegawai as $p) {
$totalMenit = $p->lemburs->sum('durasi_menit');
$jam = floor($totalMenit / 60);
$menit = $totalMenit % 60;
$rekap[$p->id] = [
'total_menit' => $totalMenit,
'format_jam' => "{$jam}j {$menit}m",
'jumlah_hari' => $p->lemburs->count(),
'poin_diperoleh' => $p->lemburs->sum('jumlah_poin'),
];
}
return view('laporan-lembur.index', compact('pegawai', 'rekap', 'bulan', 'tahun', 'search', 'divisiId', 'divisiList'));
}
public function exportExcel(Request $request)
{
$bulan = $request->input('bulan', date('m'));
$tahun = $request->input('tahun', date('Y'));
$bulan = $request->input('bulan', date('m'));
$tahun = $request->input('tahun', date('Y'));
$divisiId = $request->input('id_divisi');
$query = User::with(['divisi', 'jabatan', 'lemburs' => function($q) use ($bulan, $tahun) {
$q->whereYear('tanggal_lembur', $tahun)
->whereMonth('tanggal_lembur', $bulan)
->where('id_status', 2);
}])
->whereHas('roles', function($q) {
$q->where('nama_role', '!=', 'super_admin');
});
$authUser = Auth::user();
$isGlobalAdmin = $authUser->isGlobalAdmin();
if ($divisiId) {
$query->where('id_divisi', $divisiId);
}
[$pegawai, $rekap] = $this->buildRekap(
$bulan, $tahun, $divisiId,
$isGlobalAdmin ? null : $authUser->id_kantor
);
$pegawai = $query->orderBy('nama_lengkap', 'asc')->get();
// Guard: cek apakah ada data lembur di periode tersebut
$adaDataLembur = $pegawai->contains(fn($p) => $p->lemburs->isNotEmpty());
if (!$adaDataLembur) {
return redirect()->back()->with('error', 'Tidak ada data lembur yang disetujui untuk diekspor pada periode tersebut.');
}
$filename = "Laporan_Lembur_{$bulan}_{$tahun}.xlsx";
return Excel::download(new LaporanLemburExport($pegawai, $rekap, $bulan, $tahun), $filename);
}
private function buildRekap(string $bulan, string $tahun, $divisiId = null, $kantorId = null, $search = null): array
{
$query = User::with(['divisi', 'jabatan', 'lemburs' => function ($q) use ($bulan, $tahun) {
$q->whereYear('tanggal_lembur', $tahun)
->whereMonth('tanggal_lembur', $bulan)
->where('id_status', StatusPengajuan::DISETUJUI);
}])
->bukanHrd();
if ($kantorId) {
$query->where('id_kantor', $kantorId);
}
if ($divisiId) {
$query->where('id_divisi', $divisiId);
}
if ($search) {
$query->where(function ($q) use ($search) {
$q->where('nama_lengkap', 'like', '%' . $search . '%')
->orWhere('nik', 'like', '%' . $search . '%');
});
}
$pegawai = $query->orderBy('nama_lengkap', 'asc')->paginate(15)->withQueryString();
$rekap = [];
foreach ($pegawai as $p) {
$totalMenit = $p->lemburs->sum('durasi_menit');
$jam = floor($totalMenit / 60);
$menit = $totalMenit % 60;
$jam = floor($totalMenit / 60);
$menit = $totalMenit % 60;
$rekap[$p->id] = [
'total_menit' => $totalMenit,
@ -101,8 +100,6 @@ public function exportExcel(Request $request)
];
}
$filename = "Laporan_Lembur_{$bulan}_{$tahun}.xlsx";
return Excel::download(new LaporanLemburExport($pegawai, $rekap, $bulan, $tahun), $filename);
return [$pegawai, $rekap];
}
}

View File

@ -10,6 +10,7 @@
use App\Services\LemburService;
use App\Services\NotifikasiService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
class LemburController extends Controller
@ -23,10 +24,10 @@ public function __construct(LemburService $lemburService)
public function index()
{
$user = \Illuminate\Support\Facades\Auth::user();
$user = Auth::user();
$isGlobalAdmin = $user->isGlobalAdmin();
$query = Lembur::with(['user', 'status', 'kompensasi']);
$query = Lembur::with(['user.jabatan', 'status', 'kompensasi']);
if (!$isGlobalAdmin) {
$query->whereHas('user', function ($q) use ($user) {
@ -43,7 +44,14 @@ public function index()
public function create()
{
$pegawai = User::where('status_aktif', 1)->orderBy('nama_lengkap', 'asc')->get();
$user = Auth::user();
$query = User::where('status_aktif', 1);
if (!$user->isGlobalAdmin()) {
$query->where('id_kantor', $user->id_kantor);
}
$pegawai = $query->orderBy('nama_lengkap', 'asc')->get();
$kompensasi = JenisKompensasi::all();
return view('lembur.create', compact('pegawai', 'kompensasi'));
}
@ -53,19 +61,40 @@ public function store(Request $request)
$request->validate([
'id_user' => 'required|exists:users,id',
'tanggal_lembur' => 'required|date',
'jam_mulai' => 'required',
'jam_selesai' => 'required',
'jam_mulai' => 'required|date_format:H:i',
'jam_selesai' => 'required|date_format:H:i|different:jam_mulai',
'id_kompensasi' => 'required|exists:jenis_kompensasi,id_kompensasi',
'keterangan' => 'nullable|string'
'keterangan' => 'nullable|string|max:255'
]);
$userAuth = Auth::user();
$targetUser = User::findOrFail($request->id_user);
if (!$userAuth->isGlobalAdmin() && $targetUser->id_kantor != $userAuth->id_kantor) {
return redirect()->back()->with('error', 'Anda tidak diizinkan membuat lembur untuk pegawai kantor lain.')->withInput();
}
try {
DB::transaction(function () use ($request) {
$user = User::findOrFail($request->id_user);
$lembur = $this->lemburService->createLembur($user, $request->all());
$lembur = null;
DB::transaction(function () use ($request, $targetUser, &$lembur) {
$lembur = $this->lemburService->createLembur($targetUser, $request->all());
$this->lemburService->approve($lembur);
});
if ($lembur) {
$message = 'Lembur manual Anda pada tanggal ' . $lembur->tanggal_lembur->translatedFormat('d F Y') . ' telah ditambahkan dan disetujui oleh HRD.';
if ($lembur->id_kompensasi == JenisKompensasiEnum::TAMBAHAN_POIN) {
$message .= ' Poin telah ditambahkan ke saldo Anda.';
}
app(NotifikasiService::class)->kirim(
$targetUser->id,
'lembur_disetujui',
'Lembur Manual Disetujui',
$message,
['id_lembur' => $lembur->id_lembur]
);
}
return redirect()->route('lembur.index')->with('success', 'Data lembur manual berhasil ditambahkan dan disetujui otomatis.');
} catch (\Exception $e) {
return redirect()->back()->with('error', 'Gagal menambahkan lembur: ' . $e->getMessage())->withInput();
@ -76,11 +105,11 @@ public function update(Request $request, Lembur $lembur)
{
$request->validate([
'action' => 'required|in:approve,reject',
'alasan_penolakan' => 'required_if:action,reject|nullable|string'
'alasan_penolakan' => 'required_if:action,reject|nullable|string|max:500'
]);
$lembur->load('user');
$userAuth = \Illuminate\Support\Facades\Auth::user();
$userAuth = Auth::user();
if (!$userAuth->isGlobalAdmin() && $lembur->user->id_kantor != $userAuth->id_kantor) {
return redirect()->back()->with('error', 'Anda tidak diizinkan memproses data dari kantor lain.');
}

View File

@ -17,10 +17,7 @@ class PegawaiController extends Controller
{
public function index(Request $request)
{
$query = User::with(['divisi', 'jabatan', 'kantor', 'roles'])
->whereDoesntHave('roles', function ($q) {
$q->where('nama_role', 'hrd');
});
$query = User::with(['divisi', 'jabatan', 'kantor', 'roles'])->bukanHrd();
if ($request->filled('filter_jabatan')) {
$query->where('id_jabatan', $request->filter_jabatan);
@ -48,21 +45,14 @@ public function index(Request $request)
$allKantor = Kantor::withCount([
'users as total_pegawai' => function ($q) {
$q->whereDoesntHave('roles', function ($sq) {
$sq->where('nama_role', 'hrd');
});
$q->bukanHrd();
}
])->get();
$stats = [
'kantor_list' => $allKantor,
'total' => User::whereDoesntHave('roles', function ($q) {
$q->where('nama_role', 'hrd');
})->count(),
'active' => User::where('status_aktif', 1)
->whereDoesntHave('roles', function ($q) {
$q->where('nama_role', 'hrd');
})->count(),
'total' => User::bukanHrd()->count(),
'active' => User::bukanHrd()->where('status_aktif', 1)->count(),
];
$allJabatan = Jabatan::all();

View File

@ -7,8 +7,13 @@
use App\Enums\StatusPresensi;
use App\Enums\StatusSurat;
use App\Enums\StatusValidasi;
use App\Http\Requests\StorePengajuanIzinRequest;
use App\Models\Divisi;
use App\Models\JenisIzin;
use App\Models\Kantor;
use App\Models\PengajuanIzin;
use App\Models\Presensi;
use App\Models\StatusPengajuan as StatusPengajuanModel;
use App\Models\User;
use App\Models\SuratIzin;
use App\Models\TandaTangan;
@ -47,28 +52,31 @@ public function index(Request $request)
$izin = $query->orderBy('created_at', 'desc')->paginate(15);
$statuses = \App\Models\StatusPengajuan::all();
$jenisIzin = \App\Models\JenisIzin::all();
$users = \App\Models\User::with(['kantor', 'divisi', 'jabatan'])->select('id', 'nama_lengkap', 'nik', 'id_kantor', 'id_divisi', 'id_jabatan')->orderBy('nama_lengkap', 'asc')->get();
$kantor = \App\Models\Kantor::all();
$divisi = \App\Models\Divisi::all();
$statuses = StatusPengajuanModel::orderBy('id_status')->get();
$jenisIzinList = JenisIzin::all();
return view('izin.index', compact('izin', 'statusId', 'statuses', 'jenisIzin', 'users', 'kantor', 'divisi'));
$usersQuery = User::with(['kantor', 'divisi', 'jabatan'])
->select('id', 'nama_lengkap', 'nik', 'id_kantor', 'id_divisi', 'id_jabatan')
->where('status_aktif', 1)
->bukanHrd()
->orderBy('nama_lengkap', 'asc');
if (!$isGlobalAdmin) {
$usersQuery->where('id_kantor', $user->id_kantor);
}
$users = $usersQuery->get();
$kantor = $isGlobalAdmin ? Kantor::all() : Kantor::where('id_kantor', $user->id_kantor)->get();
$divisi = Divisi::all();
return view('izin.index', compact('izin', 'statusId', 'statuses', 'jenisIzinList', 'users', 'kantor', 'divisi'));
}
public function store(Request $request)
public function store(StorePengajuanIzinRequest $request)
{
$request->validate([
'id_user' => 'required|exists:users,id',
'id_jenis_izin' => 'required|exists:jenis_izin,id_jenis_izin',
'tanggal_mulai' => 'required|date|after_or_equal:today',
'tanggal_selesai' => 'required|date|after_or_equal:tanggal_mulai',
'alasan' => 'required|string',
'bukti_file' => 'nullable|file|mimes:jpeg,png,jpg,pdf|max:2048'
]);
$jenisIzin = \App\Models\JenisIzin::find($request->id_jenis_izin);
if ($jenisIzin && $jenisIzin->nama_izin == 'Cuti') {
$jenisIzinData = JenisIzin::find($request->id_jenis_izin);
if ($jenisIzinData && $jenisIzinData->id_jenis_izin == JenisIzinEnum::CUTI) {
$pegawai = User::find($request->id_user);
$tanggalMulai = Carbon::parse($request->tanggal_mulai);
$tanggalSelesai = Carbon::parse($request->tanggal_selesai);
@ -88,12 +96,12 @@ public function store(Request $request)
}
}
if ($jenisIzin && $jenisIzin->id_jenis_izin == \App\Enums\JenisIzin::SAKIT) {
if ($jenisIzinData && $jenisIzinData->id_jenis_izin == JenisIzinEnum::SAKIT) {
$tanggalMulai = Carbon::parse($request->tanggal_mulai);
$jumlahHari = $tanggalMulai->diffInDays(Carbon::parse($request->tanggal_selesai)) + 1;
$sudahSakitBulanIni = PengajuanIzin::where('id_user', $request->id_user)
->where('id_jenis_izin', \App\Enums\JenisIzin::SAKIT)
->where('id_jenis_izin', JenisIzinEnum::SAKIT)
->whereIn('id_status', [StatusPengajuan::PENDING, StatusPengajuan::DISETUJUI])
->whereMonth('tanggal_mulai', $tanggalMulai->month)
->whereYear('tanggal_mulai', $tanggalMulai->year)
@ -127,7 +135,7 @@ public function store(Request $request)
return back()->withInput()->with('error', 'Pegawai sudah memiliki pengajuan Izin/Cuti (Pending/Disetujui) pada rentang tanggal tersebut!');
}
$existingPresensi = \App\Models\Presensi::where('id_user', $userId)
$existingPresensi = Presensi::where('id_user', $userId)
->whereBetween('tanggal', [$tglMulai, $tglSelesai])
->whereNotNull('jam_masuk')
->exists();
@ -260,10 +268,9 @@ public function approve($id)
public function reject(Request $request, $id)
{
$izin = PengajuanIzin::with('user')->findOrFail($id);
$izin = PengajuanIzin::with(['user', 'jenisIzin'])->findOrFail($id);
$user = Auth::user();
// Security Check
if (!$user->isGlobalAdmin() && $izin->user->id_kantor != $user->id_kantor) {
return redirect()->back()->with('error', 'Anda tidak diizinkan menolak pengajuan dari kantor lain.');
}
@ -276,18 +283,29 @@ public function reject(Request $request, $id)
return redirect()->back()->with('error', 'Pengajuan Cuti harus ditolak melalui menu Surat Izin.');
}
$izin->update([
'id_status' => StatusPengajuan::DITOLAK
]);
try {
DB::beginTransaction();
app(NotifikasiService::class)->kirim(
$izin->id_user,
'izin_ditolak',
'Pengajuan Izin Ditolak',
'Pengajuan ' . ($izin->jenisIzin->nama_izin ?? 'Izin') . ' Anda ditolak.',
['id_izin' => $izin->id_izin]
);
$izin->update([
'id_status' => StatusPengajuan::DITOLAK,
'alasan_penolakan' => $request->alasan_penolakan,
]);
return redirect()->back()->with('success', 'Pengajuan izin berhasil ditolak.');
DB::commit();
app(NotifikasiService::class)->kirim(
$izin->id_user,
'izin_ditolak',
'Pengajuan Izin Ditolak',
'Pengajuan ' . ($izin->jenisIzin->nama_izin ?? 'Izin') . ' Anda ditolak. Catatan: ' . ($request->alasan_penolakan ?? '-'),
['id_izin' => $izin->id_izin]
);
return redirect()->back()->with('success', 'Pengajuan izin berhasil ditolak.');
} catch (\Exception $e) {
DB::rollBack();
return redirect()->back()->with('error', 'Gagal menolak pengajuan: ' . $e->getMessage());
}
}
}

View File

@ -7,6 +7,7 @@
use App\Services\PoinService;
use App\Services\NotifikasiService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
class PenggunaanPoinController extends Controller
@ -20,10 +21,10 @@ public function __construct(PoinService $poinService)
public function index()
{
$user = \Illuminate\Support\Facades\Auth::user();
$user = Auth::user();
$isGlobalAdmin = $user->isGlobalAdmin();
$query = PenggunaanPoin::with(['user', 'jenisPengurangan', 'status']);
$query = PenggunaanPoin::with(['user.divisi', 'jenisPengurangan', 'status']);
if (!$isGlobalAdmin) {
$query->whereHas('user', function ($q) use ($user) {
@ -48,7 +49,7 @@ public function update(Request $request, $id)
$penggunaan = PenggunaanPoin::findOrFail($id);
$penggunaan->load('user');
$userAuth = \Illuminate\Support\Facades\Auth::user();
$userAuth = Auth::user();
if (!$userAuth->isGlobalAdmin() && $penggunaan->user->id_kantor != $userAuth->id_kantor) {
return redirect()->back()->with('error', 'Anda tidak diizinkan memproses data dari kantor lain.');
}
@ -83,7 +84,7 @@ public function update(Request $request, $id)
$penggunaan->id_user,
'poin_disetujui',
'Penggunaan Poin Disetujui',
'Pengajuan penggunaan poin Anda pada tanggal ' . $penggunaan->tanggal_penggunaan . ' telah disetujui.',
'Pengajuan penggunaan poin Anda pada tanggal ' . ($penggunaan->tanggal_penggunaan ? $penggunaan->tanggal_penggunaan->translatedFormat('d F Y') : '-') . ' telah disetujui.',
['id_penggunaan' => $penggunaan->id_penggunaan]
);
@ -103,7 +104,7 @@ public function update(Request $request, $id)
$penggunaan->id_user,
'poin_ditolak',
'Penggunaan Poin Ditolak',
'Pengajuan penggunaan poin Anda pada tanggal ' . $penggunaan->tanggal_penggunaan . ' ditolak.',
'Pengajuan penggunaan poin Anda pada tanggal ' . ($penggunaan->tanggal_penggunaan ? $penggunaan->tanggal_penggunaan->translatedFormat('d F Y') : '-') . ' ditolak. Alasan: ' . ($request->alasan_penolakan ?? '-'),
['id_penggunaan' => $penggunaan->id_penggunaan]
);

View File

@ -47,7 +47,7 @@ public function store(StorePengumumanRequest $request)
'pengumuman_baru',
'📢 Pengumuman Baru',
$pengumuman->judul,
['id_pengumuman' => $pengumuman->id]
['id_pengumuman' => $pengumuman->id_pengumuman]
);
return redirect()->route('pengumuman.index')
@ -86,16 +86,20 @@ public function update(UpdatePengumumanRequest $request, Pengumuman $pengumuman)
public function destroy(Pengumuman $pengumuman)
{
if ($pengumuman->foto) {
Storage::disk('public')->delete($pengumuman->foto);
}
if ($pengumuman->lampiran) {
Storage::disk('public')->delete($pengumuman->lampiran);
}
try {
if ($pengumuman->foto) {
Storage::disk('public')->delete($pengumuman->foto);
}
if ($pengumuman->lampiran) {
Storage::disk('public')->delete($pengumuman->lampiran);
}
$pengumuman->delete();
$pengumuman->delete();
return redirect()->route('pengumuman.index')
->with('success', 'Pengumuman berhasil dihapus.');
return redirect()->route('pengumuman.index')
->with('success', 'Pengumuman berhasil dihapus.');
} catch (\Exception $e) {
return redirect()->back()->with('error', 'Gagal menghapus pengumuman: ' . $e->getMessage());
}
}
}

View File

@ -14,6 +14,7 @@
use App\Http\Requests\StoreManualPresensiRequest;
use Carbon\Carbon;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
@ -228,6 +229,7 @@ public function create()
$isGlobalAdmin = $user->isGlobalAdmin();
$pegawaiQuery = \App\Models\User::with('divisi')
->bukanHrd()
->orderBy('nama_lengkap', 'asc');
if (!$isGlobalAdmin) {
@ -312,7 +314,7 @@ public function updateManual(Request $request, $id)
'alasan_telat' => 'required|string|max:255'
], [
'alasan_telat.required' => 'Catatan koreksi wajib diisi sebagai dokumentasi log.',
'jam_masuk.required' => 'Jam masuk wajib diisi untuk kehadiran Hadir/Terlambat.'
'jam_masuk.required' => 'Jam masuk wajib diisi untuk kehadiran Hadir/Terlambat.'
]);
$presensi = Presensi::findOrFail($id);
@ -425,7 +427,7 @@ public function store(StorePresensiRequest $request)
$faceMessage = null;
try {
$faceService = new \App\Services\FaceRecognitionService();
$faceService = app(\App\Services\FaceRecognitionService::class);
$result = $faceService->verifyFace($user->id, $fotoFile);
if (isset($result['verified']) && $result['verified'] === true) {
@ -434,6 +436,10 @@ public function store(StorePresensiRequest $request)
$faceMessage = $result['message'] ?? null;
} catch (\Exception $e) {
Log::warning('Verifikasi wajah masuk gagal', [
'user_id' => $user->id,
'error' => $e->getMessage(),
]);
}
$fotoFile->storeAs('uploads/absensi', $imageName, 'public');
@ -486,12 +492,16 @@ private function handlePulang($request, $user, $hariIni, $jamSekarang)
$verifikasiPulang = 0;
try {
$faceService = new \App\Services\FaceRecognitionService();
$faceService = app(\App\Services\FaceRecognitionService::class);
$result = $faceService->verifyFace($user->id, $fotoFile);
if (isset($result['verified']) && $result['verified'] === true) {
$verifikasiPulang = 1;
}
} catch (\Exception $e) {
Log::warning('Verifikasi wajah pulang gagal', [
'user_id' => $user->id,
'error' => $e->getMessage(),
]);
}
$fotoFile->storeAs('uploads/absensi', $imageName, 'public');

View File

@ -2,34 +2,25 @@
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Http\Requests\UpdateProfileRequest;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\Rule;
class ProfileController extends Controller
{
public function edit()
{
$user = Auth::user();
return view('profile.edit', compact('user'));
return view('profile.edit');
}
public function update(Request $request)
public function update(UpdateProfileRequest $request)
{
$user = Auth::user();
$request->validate([
'nama_lengkap' => 'required|string|max:255',
'email' => ['required', 'string', 'email', 'max:255', Rule::unique('users')->ignore($user->id)],
'foto' => 'nullable|image|mimes:jpeg,png,jpg,gif|max:2048',
'password' => 'nullable|string|min:8|confirmed',
]);
$data = [
'nama_lengkap' => $request->nama_lengkap,
'email' => $request->email,
'email' => $request->email,
];
if ($request->filled('password')) {

View File

@ -181,7 +181,7 @@ public function approve(Request $request, $id)
public function reject(Request $request, $id)
{
try {
$surat = SuratIzin::with('user')->findOrFail($id);
$surat = SuratIzin::with(['user', 'pengajuanIzin'])->findOrFail($id);
$user = Auth::user();
$isGlobalAdmin = $user->isGlobalAdmin();

View File

@ -18,18 +18,26 @@ class TukarShiftController extends Controller
public function index()
{
$riwayat = RiwayatTukarShift::with(['user1', 'user2', 'jadwal1.shift', 'jadwal2.shift', 'execAdmin'])
->latest()
->get();
$user = Auth::user();
$query = RiwayatTukarShift::with(['user1', 'user2', 'jadwal1.shift', 'jadwal2.shift', 'execAdmin'])
->latest();
if (!$user->isGlobalAdmin()) {
$query->where(function ($q) use ($user) {
$q->whereHas('user1', fn($u) => $u->where('id_kantor', $user->id_kantor))
->orWhereHas('user2', fn($u) => $u->where('id_kantor', $user->id_kantor));
});
}
$riwayat = $query->get();
return view('tukar-shift.index', compact('riwayat'));
}
public function create()
{
$pegawai = User::with('kantor')->whereDoesntHave('roles', function ($q) {
$q->where('nama_role', 'hrd');
})->get();
$pegawai = User::with('kantor')->bukanHrd()->get();
$kantor = \App\Models\Kantor::orderBy('nama_kantor', 'asc')->get();

View File

@ -0,0 +1,35 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class BulkDestroyJadwalRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'tanggal_mulai' => ['required', 'date'],
'tanggal_selesai' => ['required', 'date', 'after_or_equal:tanggal_mulai'],
'user_ids' => ['required', 'array', 'min:1'],
'user_ids.*' => ['exists:users,id'],
];
}
public function messages(): array
{
return [
'tanggal_mulai.required' => 'Tanggal mulai wajib diisi.',
'tanggal_selesai.required' => 'Tanggal selesai wajib diisi.',
'tanggal_selesai.after_or_equal' => 'Tanggal selesai tidak boleh sebelum tanggal mulai.',
'user_ids.required' => 'Pilih minimal 1 pegawai.',
'user_ids.min' => 'Pilih minimal 1 pegawai.',
'user_ids.*.exists' => 'Salah satu pegawai yang dipilih tidak valid.',
];
}
}

View File

@ -9,18 +9,15 @@ class StoreHariLiburRequest extends FormRequest
public function authorize(): bool
{
return false;
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'tanggal' => ['required', 'date'],
'keterangan' => ['required', 'string', 'max:255'],
'id_kantor' => ['nullable', 'exists:kantor,id_kantor'],
];
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreJadwalRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'tanggal_mulai' => ['required', 'date'],
'tanggal_selesai' => ['required', 'date', 'after_or_equal:tanggal_mulai'],
'id_shift' => ['required', 'exists:shift_kerja,id_shift'],
'user_ids' => ['required', 'array', 'min:1'],
'user_ids.*' => ['exists:users,id'],
];
}
public function messages(): array
{
return [
'tanggal_mulai.required' => 'Tanggal mulai wajib diisi.',
'tanggal_selesai.required' => 'Tanggal selesai wajib diisi.',
'tanggal_selesai.after_or_equal' => 'Tanggal selesai tidak boleh sebelum tanggal mulai.',
'id_shift.required' => 'Shift kerja wajib dipilih.',
'id_shift.exists' => 'Shift yang dipilih tidak valid.',
'user_ids.required' => 'Pilih minimal 1 pegawai.',
'user_ids.min' => 'Pilih minimal 1 pegawai.',
'user_ids.*.exists' => 'Salah satu pegawai yang dipilih tidak valid.',
];
}
}

View File

@ -17,7 +17,8 @@ public function rules(): array
'nama_lengkap' => ['required', 'string', 'max:255'],
'nik' => ['nullable', 'string', 'max:50', 'unique:users,nik'],
'email' => ['required', 'email', 'unique:users,email'],
'no_telp' => ['nullable', 'string', 'max:20'],
'no_telp' =>
['nullable', 'string', 'max:15', 'regex:/^(\+62|62|0)[0-9]{8,11}$/'],
'alamat' => ['nullable', 'string'],
'id_divisi' => ['required', 'exists:divisi,id_divisi'],
'id_jabatan' => ['required', 'exists:jabatan,id_jabatan'],
@ -32,10 +33,13 @@ public function rules(): array
public function messages(): array
{
return [
'nik.unique' => 'NIK sudah terdaftar di sistem.',
'email.unique' => 'Email ini sudah digunakan oleh pegawai lain.',
'nik.unique' => 'NIK sudah terdaftar di sistem.',
'email.unique' => 'Email ini sudah digunakan oleh pegawai lain.',
'id_divisi.exists' => 'Divisi yang dipilih tidak valid.',
'id_role.exists' => 'Role yang dipilih tidak valid.',
'id_role.exists' => 'Role yang dipilih tidak valid.',
'no_telp.regex' => 'Format nomor telepon tidak valid. Gunakan format: 08xx, +62xx, atau 62xx.',
'foto.max' => 'Ukuran foto tidak boleh lebih dari 5MB.',
'sisa_cuti.min' => 'Sisa cuti tidak boleh negatif.',
];
}
}

View File

@ -20,11 +20,12 @@ public function authorize(): bool
public function rules(): array
{
return [
'tanggal_mulai' => 'required|date|after_or_equal:today',
'tanggal_selesai' => 'required|date|after_or_equal:tanggal_mulai',
'id_jenis_izin' => 'required|exists:jenis_izin,id_jenis_izin',
'alasan' => 'required|string|max:150',
'bukti_file' => 'nullable|file|mimes:pdf,jpg,jpeg,png|max:2048',
'id_user' => 'required|exists:users,id',
'id_jenis_izin' => 'required|exists:jenis_izin,id_jenis_izin',
'tanggal_mulai' => 'required|date',
'tanggal_selesai'=> 'required|date|after_or_equal:tanggal_mulai',
'alasan' => 'required|string|max:500',
'bukti_file' => 'nullable|file|mimes:pdf,jpg,jpeg,png|max:2048',
];
}
@ -36,10 +37,12 @@ public function rules(): array
public function attributes()
{
return [
'tanggal_mulai' => 'tanggal mulai',
'id_user' => 'pegawai',
'id_jenis_izin' => 'jenis izin',
'tanggal_mulai' => 'tanggal mulai',
'tanggal_selesai' => 'tanggal selesai',
'id_jenis_izin' => 'jenis izin',
'bukti_file' => 'bukti file',
'alasan' => 'alasan',
'bukti_file' => 'bukti file',
];
}

View File

@ -22,18 +22,34 @@ public function rules(): array
'email' => [
'required',
'email',
'unique:users,email,' . $this->route('pegawai')
Rule::unique('users', 'email')->ignore($id),
],
'no_telp' => ['nullable', 'string', 'max:20'],
'alamat' => ['nullable', 'string'],
'id_divisi' => ['required', 'exists:divisi,id_divisi'],
'id_jabatan' => ['required', 'exists:jabatan,id_jabatan'],
'id_kantor' => ['required', 'exists:kantor,id_kantor'],
'no_telp' => ['nullable', 'string', 'max:15', 'regex:/^(\+62|62|0)[0-9]{8,11}$/'],
'alamat' => ['nullable', 'string'],
'id_divisi' => ['required', 'exists:divisi,id_divisi'],
'id_jabatan' => ['required', 'exists:jabatan,id_jabatan'],
'id_kantor' => ['required', 'exists:kantor,id_kantor'],
'status_aktif' => ['required', 'in:0,1'],
'tgl_bergabung' => ['required', 'date'],
'foto' => ['nullable', 'image', 'max:5120'],
'sisa_cuti' => ['nullable', 'integer', 'min:0'],
'id_role' => ['required', 'exists:roles,id_role'],
'foto' => ['nullable', 'image', 'max:5120'],
'sisa_cuti' => ['nullable', 'integer', 'min:0'],
'id_role' => ['required', 'exists:roles,id_role'],
];
}
public function messages(): array
{
return [
'nik.unique' => 'NIK sudah terdaftar di sistem.',
'email.unique' => 'Email ini sudah digunakan oleh pegawai lain.',
'id_divisi.exists' => 'Divisi yang dipilih tidak valid.',
'id_jabatan.exists' => 'Jabatan yang dipilih tidak valid.',
'id_kantor.exists' => 'Kantor yang dipilih tidak valid.',
'id_role.exists' => 'Role yang dipilih tidak valid.',
'status_aktif.in' => 'Status pegawai tidak valid.',
'foto.max' => 'Ukuran foto tidak boleh lebih dari 5MB.',
'sisa_cuti.min' => 'Sisa cuti tidak boleh negatif.',
'no_telp.regex' => 'Format nomor telepon tidak valid. Gunakan format: 08xx, +62xx, atau 62xx.',
];
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\Rule;
class UpdateProfileRequest extends FormRequest
{
public function authorize(): bool
{
return Auth::check();
}
public function rules(): array
{
return [
'nama_lengkap' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', Rule::unique('users')->ignore(Auth::id())],
'foto' => ['nullable', 'image', 'mimes:jpeg,png,jpg', 'max:2048'],
'password' => ['nullable', 'string', 'min:8', 'confirmed'],
];
}
public function attributes(): array
{
return [
'nama_lengkap' => 'Nama Lengkap',
'email' => 'Alamat Email',
'foto' => 'Foto Profil',
'password' => 'Password Baru',
];
}
}

View File

@ -5,7 +5,9 @@
use App\Enums\StatusVerifikasiWajah;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable as QueueableTrait;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
@ -13,7 +15,7 @@
class RetrainAllModels implements ShouldQueue
{
use Queueable;
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $timeout = 3600;

View File

@ -65,7 +65,7 @@ protected static function booted()
'tanggal_selesai',
'alasan',
'bukti_file',
'alasan_penolakan',
];
public function user()

View File

@ -156,4 +156,11 @@ public function notifikasi(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(Notifikasi::class, 'id_user', 'id');
}
public function scopeBukanHrd($query)
{
return $query->whereDoesntHave('roles', function ($q) {
$q->where('nama_role', 'hrd');
});
}
}

View File

@ -130,13 +130,12 @@ public function verifyFace($userId, UploadedFile $fotoMasuk)
}
return [
'verified' => ($output['match'] ?? false) === true,
'confidence' => $output['confidence'] ?? null,
'svm_confidence' => $output['svm_confidence'] ?? null,
'normalized_distance' => $output['normalized_distance'] ?? null,
'verified' => ($output['match'] ?? false) === true,
'confidence' => $output['confidence'] ?? null,
'svm_confidence' => $output['svm_confidence'] ?? null,
'verification_status' => $output['verification_status'] ?? 'UNKNOWN',
'blur_score' => $output['blur_score'] ?? null,
'message' => $output['message'] ?? null,
'blur_score' => $output['blur_score'] ?? null,
'message' => $output['message'] ?? null,
];
}
}

View File

@ -48,12 +48,6 @@ public function deductPoin($userId, $jumlahPoinDibutuhkan, $idPenggunaan)
$remainingNeeded -= $take;
}
$penggunaan = PenggunaanPoin::find($idPenggunaan);
if ($penggunaan) {
$penggunaan->id_status = StatusPengajuan::DISETUJUI;
$penggunaan->save();
}
}
public function refundPoin($idPenggunaan)
@ -70,14 +64,9 @@ public function refundPoin($idPenggunaan)
}
$poinSumber->save();
}
$detail->delete();
}
$penggunaan = PenggunaanPoin::find($idPenggunaan);
if ($penggunaan) {
$penggunaan->id_status = StatusPengajuan::DITOLAK;
$penggunaan->save();
}
DetailPenggunaanPoin::where('id_penggunaan', $idPenggunaan)->delete();
}
public function getActivePoints($userId)

View File

@ -15,12 +15,12 @@ public function up(): void
Schema::create('log_verifikasi_wajah', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('id_user');
$table->decimal('confidence', 5, 4)->nullable();
$table->decimal('svm_confidence', 5, 4)->nullable();
$table->decimal('normalized_distance', 8, 4)->nullable();
$table->string('verification_status', 30)->nullable();
$table->boolean('is_match')->default(false);
$table->decimal('blur_score', 6, 1)->nullable();
$table->decimal('skor_kepercayaan', 5, 4)->nullable();
$table->decimal('skor_svm', 5, 4)->nullable();
$table->decimal('jarak_normalisasi', 8, 4)->nullable();
$table->string('status_verifikasi', 30)->nullable();
$table->boolean('apakah_cocok')->default(false);
$table->decimal('skor_blur', 6, 1)->nullable();
$table->string('tipe', 10)->default('presensi');
$table->timestamps();

View File

@ -27,7 +27,10 @@ public function run(): void
$tglMulai = now()->addDays(rand(-10, 10));
$tglSelesai = (clone $tglMulai)->addDays(rand(1, 3));
$idIzin = 'IZN-' . date('ym') . '-' . strtoupper(Str::random(5));
DB::table('pengajuan_izin')->insertOrIgnore([
'id_izin' => $idIzin,
'id_user' => $user->id,
'id_jenis_izin' => $idJenis,
'tanggal_mulai' => $tglMulai->format('Y-m-d'),

View File

@ -76,13 +76,17 @@
// ==== Polling Badge Notifikasi ====
function updateNotifBadge() {
const badge = document.getElementById('notif-badge');
if (!badge) return;
if (!badge) return; // User tidak punya akses notifikasi, hentikan tanpa fetch
fetch('/notifikasi/unread-count', {
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' }
})
.then(r => r.json())
.then(r => {
if (!r.ok) return null; // Tangani 403/redirect dengan aman
return r.json();
})
.then(data => {
if (!data) return;
const count = data.count ?? 0;
if (count > 0) {
badge.textContent = count > 99 ? '99+' : count;
@ -215,7 +219,7 @@ function confirmDelete(id) {
}
}
});
}
}
function confirmAction(event, formId, message, confirmBtnColor = '#3085d6', confirmBtnText = 'Ya, lanjutkan!') {
event.preventDefault();

View File

@ -1,6 +1,5 @@
opencv-contrib-python-headless
numpy
scikit-learn
scikit-image
joblib
Pillow

View File

@ -199,7 +199,7 @@ def verify_face(model_dir, user_id, image_path):
"expected_user": expected_user,
"unknown_confidence": round(unknown_confidence, 4),
"blur_score": round(blur_score, 1),
"user_id": int(user_id)
"user_id": int(user_id) if str(user_id).isdigit() else user_id
}
print(json.dumps(result))

View File

@ -1,4 +1,14 @@
@props(['label' => null, 'name', 'value' => ''])
@props(['label' => null, 'name', 'value' => ''])
@once
<style>
input[type="date"]::-webkit-calendar-picker-indicator,
input[type="date"]::-webkit-inner-spin-button {
display: none;
-webkit-appearance: none;
}
</style>
@endonce
<div class="space-y-1">
@if($label)
@ -6,15 +16,6 @@
@endif
<div class="relative">
<style>
/* Sembunyikan icon default browser */
input[type="date"]::-webkit-calendar-picker-indicator,
input[type="date"]::-webkit-inner-spin-button {
display: none;
-webkit-appearance: none;
}
</style>
<input type="date"
name="{{ $name }}"
value="{{ old($name, $value) }}"

View File

@ -140,7 +140,8 @@ function openEditModal(id, nama, sisa) {
document.getElementById('editSisa').value = sisa;
let form = document.getElementById('formEditCuti');
form.action = `/cuti/${id}`;
let baseUrl = "{{ route('cuti.update', ':id') }}";
form.action = baseUrl.replace(':id', id);
window.dispatchEvent(new CustomEvent('open-modal', { detail: 'edit-cuti' }));
}

View File

@ -224,7 +224,7 @@ class="text-xs text-primary hover:underline font-medium">Lihat</a>
</div>
@endsection
@push('scripts')
@section('script')
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
const ctx = document.getElementById('attendanceChart').getContext('2d');
@ -261,4 +261,4 @@ class="text-xs text-primary hover:underline font-medium">Lihat</a>
}
});
</script>
@endpush
@endsection

View File

@ -1,7 +1,25 @@
@extends('layouts.app')
@extends('layouts.app')
@section('title', 'Data Divisi')
@section('script')
<script>
window.openEditModal = function (btn) {
const id = btn.dataset.id;
const nama = btn.dataset.nama;
document.getElementById('edit-id').value = id;
document.getElementById('edit-nama').value = nama;
let form = document.getElementById('form-edit');
let baseUrl = "{{ route('divisi.update', ':id') }}";
form.action = baseUrl.replace(':id', id);
window.dispatchEvent(new CustomEvent('open-modal', { detail: 'edit-divisi' }));
}
</script>
@endsection
@section('content')
<div class="space-y-6">
@ -84,12 +102,7 @@ class="px-4 py-2 text-sm font-medium text-white bg-primary rounded-xl hover:bg-p
<input type="hidden" id="edit-id" name="id">
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1">Nama Divisi</label>
<input type="text" id="edit-nama" name="nama_divisi" required
class="w-full px-4 py-2 border border-slate-300 rounded-xl focus:ring-2 focus:ring-[#130F26] outline-none transition">
</div>
<x-input label="Nama Divisi" name="nama_divisi" id="edit-nama" required />
<div class="flex justify-end gap-3 pt-4">
<button type="button" x-data @click="$dispatch('close-modal', 'edit-divisi')"
@ -100,20 +113,5 @@ class="px-4 py-2 text-sm font-medium text-white bg-amber-500 rounded-xl hover:bg
</form>
</x-modal>
<script>
window.openEditModal = function (btn) {
const id = btn.dataset.id;
const nama = btn.dataset.nama;
document.getElementById('edit-id').value = id;
document.getElementById('edit-nama').value = nama;
let form = document.getElementById('form-edit');
let baseUrl = "{{ route('divisi.update', ':id') }}";
form.action = baseUrl.replace(':id', id);
window.dispatchEvent(new CustomEvent('open-modal', { detail: 'edit-divisi' }));
}
</script>
@endsection

View File

@ -1,3 +1,4 @@
@php use App\Enums\StatusVerifikasiWajah; @endphp
@extends('layouts.app')
@section('title', 'Verifikasi Wajah')
@ -109,12 +110,13 @@ class="!mb-0" />
@forelse ($users as $user)
@php
$dw = $user->dataWajah;
$faceStatus = 'unregistered';
if ($dw) {
if ($dw->is_verified == 0) $faceStatus = 'pending';
elseif ($dw->is_verified == 1) $faceStatus = 'approved';
elseif ($dw->is_verified == 2) $faceStatus = 'rejected';
if ($dw->is_verified == StatusVerifikasiWajah::PENDING) $faceStatus = 'pending';
elseif ($dw->is_verified == StatusVerifikasiWajah::APPROVED) $faceStatus = 'approved';
elseif ($dw->is_verified == StatusVerifikasiWajah::REJECTED) $faceStatus = 'rejected';
}
@endphp
<tr class="hover:bg-slate-50 transition-colors">

View File

@ -1,15 +1,154 @@
@extends('layouts.app')
@extends('layouts.app')
@section('title', 'Data Hari Libur')
@section('style')
<script src='https://cdn.jsdelivr.net/npm/fullcalendar@6.1.8/index.global.min.js'></script>
<style>
.fc-toolbar-title { font-size: 1.15rem !important; font-weight: 700; color: #1e293b; }
.fc-col-header-cell { background-color: #f8fafc; padding: 8px 0; border-color: #e2e8f0; }
.fc-daygrid-day-number { color: #64748b; font-weight: 500; font-size: 0.85rem; }
.fc-day-today { background-color: #f0f9ff !important; }
.fc-event { border: none; padding: 2px; }
.fc-event {
cursor: default;
border: none !important;
outline: none !important;
box-shadow: none !important;
background: transparent;
}
.fc-daygrid-event {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.fc-toolbar-title {
font-size: 1.25rem !important;
font-weight: 700;
color: #1e293b;
}
.fc-col-header-cell {
background-color: #f8fafc;
padding: 12px 0;
border-color: #e2e8f0;
}
.fc-daygrid-day-number {
color: #64748b;
font-weight: 500;
}
.fc-day-today {
background-color: #ffffff !important;
}
.fc-event-title {
font-weight: 600;
line-height: 1.2;
}
.fc-popover-body {
max-height: 250px !important;
overflow-y: auto !important;
}
#holiday-calendar {
min-height: 550px;
font-family: inherit;
}
.fc-theme-standard td, .fc-theme-standard th, .fc-theme-standard .fc-scrollgrid {
border-color: #f1f5f9;
}
.fc-theme-standard .fc-scrollgrid { border-radius: 12px; overflow: hidden; border: 1px solid #e2e8f0; }
.fc-col-header-cell {
background-color: #f8fafc;
padding: 12px 0 !important;
}
.fc-col-header-cell-cushion {
color: #64748b;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
text-decoration: none !important;
}
.fc-daygrid-day-top {
justify-content: center !important;
padding-top: 8px;
padding-bottom: 4px;
}
.fc-daygrid-day-number {
color: #475569;
font-weight: 600;
font-size: 0.875rem;
text-decoration: none !important;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 9999px;
transition: all 0.2s;
}
.fc-daygrid-day-number:hover {
background-color: #f1f5f9;
}
.fc-day-today .fc-daygrid-day-number {
color: #ffffff !important;
background-color: #3b82f6 !important;
box-shadow: 0 2px 4px rgba(59, 130, 246, 0.3);
}
.fc-daygrid-day-events {
padding: 0 6px !important;
}
.fc-daygrid-event-harness {
margin-bottom: 6px !important;
}
.fc-event {
border: none !important;
background: transparent !important;
box-shadow: none !important;
border-radius: 6px !important;
overflow: hidden;
}
.fc-daygrid-more-link {
color: #ef4444 !important;
font-weight: 700;
font-size: 0.7rem;
padding: 5px 8px;
background-color: #fef2f2;
border-radius: 6px;
display: block;
text-align: center;
margin: 4px 6px 8px 6px;
transition: all 0.2s;
text-decoration: none !important;
}
.fc-daygrid-more-link:hover {
background-color: #fee2e2;
color: #dc2626 !important;
}
.fc .fc-toolbar-title {
font-size: 1.25rem !important;
font-weight: 800;
color: #0f172a;
}
.fc .fc-button-primary {
background-color: #ffffff !important;
color: #475569 !important;
border: 1px solid #e2e8f0 !important;
border-radius: 8px !important;
font-weight: 600;
text-transform: capitalize;
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
padding: 6px 16px !important;
transition: all 0.2s;
}
.fc .fc-button-primary:hover {
background-color: #f8fafc !important;
color: #0f172a !important;
border-color: #cbd5e1 !important;
}
.fc .fc-button-primary:not(:disabled).fc-button-active,
.fc .fc-button-primary:not(:disabled):active {
background-color: #f1f5f9 !important;
color: #0f172a !important;
border-color: #cbd5e1 !important;
box-shadow: inset 0 2px 4px 0 rgb(0 0 0 / 0.05) !important;
}
.fc-toolbar.fc-header-toolbar {
margin-bottom: 24px !important;
}
</style>
@endsection
@ -22,7 +161,7 @@
<x-filter-select name="id_kantor" onchange="this.form.submit()" class="py-1.5 text-xs w-48">
<option value="">Semua Kantor (Global)</option>
@foreach($kantors as $k)
<option value="{{ $k->id_kantor }}" request('id_kantor') == $k->id_kantor ? 'selected' : ''>{{ $k->nama_kantor }}</option>
<option value="{{ $k->id_kantor }}" {{ request('id_kantor') == $k->id_kantor ? 'selected' : '' }}>{{ $k->nama_kantor }}</option>
@endforeach
</x-filter-select>
</form>
@ -48,7 +187,7 @@
<svg class="w-5 h-5 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg>
Kalender Libur Nasional & Perusahaan
</h3>
<div id="calendar" class="min-h-[450px]"></div>
<div id="holiday-calendar" class="min-h-[450px]"></div>
</div>
<div class="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
@ -154,6 +293,9 @@
</form>
</x-modal>
@endsection
@section('script')
<script>
window.openEditModal = function (btn) {
const id = btn.dataset.id;
@ -176,7 +318,7 @@
window.holidayCalendar = window.holidayCalendar || null;
function initHolidayCalendar() {
var calendarEl = document.getElementById('calendar');
var calendarEl = document.getElementById('holiday-calendar');
if (!calendarEl) return;
if (window.holidayCalendar) {
@ -184,6 +326,12 @@ function initHolidayCalendar() {
window.holidayCalendar = null;
}
// Paksa destroy jadwal calendar jika masih hidup dari halaman penjadwalan
if (window.jadwalCalendar) {
window.jadwalCalendar.destroy();
window.jadwalCalendar = null;
}
const rawData = @json($hariLiburs->items() ?? $hariLiburs);
const calendarEvents = rawData.map(function(hl) {
@ -204,14 +352,25 @@ function initHolidayCalendar() {
center: 'title',
right: 'dayGridMonth'
},
buttonText: {
today: 'Hari Ini',
month: 'Bulan',
},
events: calendarEvents,
height: 'auto',
displayEventTime: false,
dayMaxEvents: 3,
moreLinkText: 'lainnya',
moreLinkClick: 'popover',
eventContent: function(arg) {
return {
html: `<div style="background:#fef2f2; color:#dc2626; padding: 2px 4px; border-radius: 4px; border-left: 3px solid #ef4444; font-weight:700; font-size: 10px; white-space: normal; line-height: 1.2;">
${arg.event.title}
</div>`
};
let contentEl = document.createElement('div');
contentEl.innerHTML = `
<div class="bg-red-50 text-red-700 px-2 py-1.5 rounded-lg border border-red-200 font-bold text-[10px] flex items-center gap-1.5 w-full truncate shadow-sm">
<span class="w-2 h-2 rounded-full bg-red-500 flex-shrink-0"></span>
<span class="truncate">${arg.event.title}</span>
</div>
`;
return { domNodes: [contentEl] };
}
});
@ -222,7 +381,7 @@ function initHolidayCalendar() {
window.__holidayEventAttached = true;
document.addEventListener('turbo:load', function () {
if (document.getElementById('calendar') && typeof initHolidayCalendar === 'function') {
if (document.getElementById('holiday-calendar') && typeof initHolidayCalendar === 'function') {
initHolidayCalendar();
}
});
@ -236,12 +395,12 @@ function initHolidayCalendar() {
}
if (document.readyState === 'complete' || document.readyState === 'interactive') {
if (document.getElementById('calendar') && !window.holidayCalendar) {
if (document.getElementById('holiday-calendar') && !window.holidayCalendar) {
setTimeout(initHolidayCalendar, 50);
}
} else {
document.addEventListener('DOMContentLoaded', function () {
if (document.getElementById('calendar') && !window.holidayCalendar) {
if (document.getElementById('holiday-calendar') && !window.holidayCalendar) {
setTimeout(initHolidayCalendar, 50);
}
});

View File

@ -31,9 +31,9 @@ class="!mb-0 w-full md:w-64" />
<x-filter-select name="status"
onchange="window.location.href='{{ route('izin.index') }}?status=' + this.value + '&search={{ request('search') }}'">
<option value="">Semua Status</option>
<option value="1" {{ $statusId == 1 ? 'selected' : '' }}>Pending</option>
<option value="2" {{ $statusId == 2 ? 'selected' : '' }}>Disetujui</option>
<option value="3" {{ $statusId == 3 ? 'selected' : '' }}>Ditolak</option>
<option value="{{ \App\Enums\StatusPengajuan::PENDING }}" {{ $statusId == \App\Enums\StatusPengajuan::PENDING ? 'selected' : '' }}>Pending</option>
<option value="{{ \App\Enums\StatusPengajuan::DISETUJUI }}" {{ $statusId == \App\Enums\StatusPengajuan::DISETUJUI ? 'selected' : '' }}>Disetujui</option>
<option value="{{ \App\Enums\StatusPengajuan::DITOLAK }}" {{ $statusId == \App\Enums\StatusPengajuan::DITOLAK ? 'selected' : '' }}>Ditolak</option>
</x-filter-select>
</div>
</x-page-header>
@ -66,7 +66,7 @@ class="w-10 h-10 rounded-full bg-slate-100 flex items-center justify-center text
<span
class="px-2.5 py-1 rounded-full text-xs font-medium
{{ $item->id_jenis_izin == 1 ? 'bg-rose-50 text-rose-600' : 'bg-blue-50 text-blue-600' }}">
{{ $item->id_jenis_izin == \App\Enums\JenisIzin::SAKIT ? 'bg-rose-50 text-rose-600' : 'bg-blue-50 text-blue-600' }}">
{{ $item->jenisIzin->nama_izin ?? 'Unknown' }}
</span>
</td>
@ -81,12 +81,12 @@ class="px-2.5 py-1 rounded-full text-xs font-medium
@php
$statusName = $item->statusPengajuan->nama_status ?? '-';
$badgeColor = match ($item->id_status) {
2 => 'green',
3 => 'red',
\App\Enums\StatusPengajuan::DISETUJUI => 'green',
\App\Enums\StatusPengajuan::DITOLAK => 'red',
default => 'yellow'
};
@endphp
@if($item->id_jenis_izin == 2 && $item->id_status == 1)
@if($item->id_jenis_izin == \App\Enums\JenisIzin::CUTI && $item->id_status == \App\Enums\StatusPengajuan::PENDING)
<x-badge color="blue">Via Surat Izin</x-badge>
@else
<x-badge color="{{ $badgeColor }}">{{ $statusName }}</x-badge>
@ -94,12 +94,23 @@ class="px-2.5 py-1 rounded-full text-xs font-medium
</td>
<td class="px-6 py-4 text-right">
<div class="flex items-center justify-end gap-2">
<button data-item="{{ $item->makeHidden('suratIzin')->toJson() }}"
<button data-item="{{ json_encode([
'id_izin' => $item->id_izin,
'id_status' => $item->id_status,
'id_jenis_izin' => $item->id_jenis_izin,
'tanggal_mulai' => $item->tanggal_mulai,
'tanggal_selesai' => $item->tanggal_selesai,
'alasan' => $item->alasan,
'alasan_penolakan' => $item->alasan_penolakan,
'bukti_file' => $item->bukti_file,
'user' => ['nama_lengkap' => $item->user?->nama_lengkap, 'nik' => $item->user?->nik],
'jenis_izin' => ['nama_izin' => $item->jenisIzin?->nama_izin],
]) }}"
onclick="openDetailModal(JSON.parse(this.getAttribute('data-item')))"
class="px-3 py-1.5 text-xs font-medium text-slate-600 bg-white border border-slate-300 rounded-lg hover:bg-slate-50 transition shadow-sm">
Detail
</button>
@if($item->id_jenis_izin == 2 && $item->suratIzin)
@if($item->id_jenis_izin == \App\Enums\JenisIzin::CUTI && $item->suratIzin)
<a href="{{ route('surat-izin.show', $item->suratIzin->id_surat) }}"
class="px-3 py-1.5 text-xs font-medium text-blue-600 bg-blue-50 border border-blue-200 rounded-lg hover:bg-blue-100 transition shadow-sm">
Lihat Surat
@ -150,7 +161,7 @@ class="px-3 py-1.5 text-xs font-medium text-blue-600 bg-blue-50 border border-bl
<div>
<x-select label="Jenis Izin" name="id_jenis_izin" required>
@foreach($jenisIzin as $j)
@foreach($jenisIzinList as $j)
<option value="{{ $j->id_jenis_izin }}">{{ $j->nama_izin }}</option>
@endforeach
</x-select>
@ -165,7 +176,7 @@ class="px-3 py-1.5 text-xs font-medium text-blue-600 bg-blue-50 border border-bl
<x-textarea label="Alasan" name="alasan" required rows="3" />
</div>
<x-input type="file" label="Bukti Dokumen (Gambar/PDF)" name="bukti_file" required
<x-input type="file" label="Bukti Dokumen (Gambar/PDF)" name="bukti_file"
accept=".jpeg,.png,.jpg,.pdf" />
<div class="pt-4 flex justify-end gap-3">
@ -203,6 +214,11 @@ class="px-3 py-1.5 text-xs font-medium text-blue-600 bg-blue-50 border border-bl
</p>
</div>
<div id="alasan-penolakan-wrapper" class="hidden">
<label class="block text-xs font-bold text-red-500 uppercase tracking-wider mb-1">Alasan Penolakan</label>
<p class="text-sm text-red-700 bg-red-50 p-3 rounded-lg border border-red-100" id="detail-alasan-penolakan">-</p>
</div>
<div>
<label class="block text-xs font-bold text-slate-500 uppercase tracking-wider mb-1">Bukti Dokumen</label>
<div
@ -234,12 +250,13 @@ class="text-sm text-blue-600 hover:underline hidden">Download File</a>
@click="$dispatch('close-modal', 'detail-izin')">Tutup</x-button>
@auth
<form id="form-reject-izin" action="" method="POST" class="hidden">@csrf</form>
<form id="form-reject-izin" action="" method="POST" class="hidden">
@csrf
<input type="hidden" name="alasan_penolakan" id="input-alasan-penolakan">
</form>
<form id="form-approve-izin" action="" method="POST" class="hidden">@csrf</form>
<x-button id="btn-reject" variant="danger"
onclick="confirmAction(event, 'form-reject-izin', 'Status akan diubah menjadi Ditolak.', '#ef4444', 'Tolak')">
<x-button id="btn-reject" variant="danger" onclick="submitRejectWithAlasan()">
Tolak
</x-button>
<x-button id="btn-approve" variant="primary"
@ -258,6 +275,10 @@ class="text-sm text-blue-600 hover:underline hidden">Download File</a>
@section('script')
<script>
const JENIS_CUTI = {{ \App\Enums\JenisIzin::CUTI }};
const STATUS_PENDING = {{ \App\Enums\StatusPengajuan::PENDING }};
const STATUS_DITOLAK = {{ \App\Enums\StatusPengajuan::DITOLAK }};
function openDetailModal(data) {
document.getElementById('current-id-izin').value = data.id_izin;
document.getElementById('detail-nama').innerText = data.user.nama_lengkap;
@ -281,20 +302,16 @@ function openDetailModal(data) {
noBukti.classList.add('hidden');
if (data.bukti_file) {
const url = "{{ asset('storage') }}/" + data.bukti_file;
const fileUrl = "{{ asset('storage') }}/" + data.bukti_file;
const isImage = data.bukti_file.match(/\.(jpeg|jpg|png|gif)$/i) || data.bukti_file.includes('placehold.co');
if (isImage) {
img.src = url;
img.src = data.bukti_file.startsWith('http') ? data.bukti_file : fileUrl;
img.classList.remove('hidden');
} else {
link.href = url;
link.href = fileUrl;
link.classList.remove('hidden');
}
if (data.bukti_file.startsWith('http')) {
img.src = data.bukti_file;
}
} else {
noBukti.classList.remove('hidden');
}
@ -302,13 +319,20 @@ function openDetailModal(data) {
const btnApprove = document.getElementById('btn-approve');
const btnReject = document.getElementById('btn-reject');
const cutiInfo = document.getElementById('cuti-info-banner');
const alasanPenolakanWrapper = document.getElementById('alasan-penolakan-wrapper');
if (btnApprove) btnApprove.classList.add('hidden');
if (btnReject) btnReject.classList.add('hidden');
if (cutiInfo) cutiInfo.classList.add('hidden');
if (alasanPenolakanWrapper) alasanPenolakanWrapper.classList.add('hidden');
if (data.id_status == 1) {
if (data.id_jenis_izin == 2) {
if (data.id_status == STATUS_DITOLAK && data.alasan_penolakan) {
document.getElementById('detail-alasan-penolakan').innerText = data.alasan_penolakan;
if (alasanPenolakanWrapper) alasanPenolakanWrapper.classList.remove('hidden');
}
if (data.id_status == STATUS_PENDING) {
if (data.id_jenis_izin == JENIS_CUTI) {
if (cutiInfo) cutiInfo.classList.remove('hidden');
} else {
if (btnApprove) btnApprove.classList.remove('hidden');
@ -319,6 +343,15 @@ function openDetailModal(data) {
window.dispatchEvent(new CustomEvent('open-modal', { detail: 'detail-izin' }));
}
function submitRejectWithAlasan() {
const alasan = prompt('Masukkan alasan penolakan (opsional):');
if (alasan === null) return;
const inputAlasan = document.getElementById('input-alasan-penolakan');
if (inputAlasan) inputAlasan.value = alasan;
const form = document.getElementById('form-reject-izin');
if (form) form.submit();
}
function initCreateIzinForm() {
const createForm = document.querySelector('form[action="{{ route('izin.store') }}"]');
if (createForm) {

View File

@ -1,7 +1,25 @@
@extends('layouts.app')
@extends('layouts.app')
@section('title', 'Data Jabatan')
@section('script')
<script>
window.openEditModal = function (btn) {
const id = btn.dataset.id;
const nama = btn.dataset.nama;
document.getElementById('edit-id').value = id;
document.getElementById('edit-nama').value = nama;
let form = document.getElementById('form-edit');
let baseUrl = "{{ route('jabatan.update', ':id') }}";
form.action = baseUrl.replace(':id', id);
window.dispatchEvent(new CustomEvent('open-modal', { detail: 'edit-jabatan' }));
}
</script>
@endsection
@section('content')
<div class="space-y-6">
@ -80,11 +98,7 @@ class="px-4 py-2 text-sm font-medium text-white bg-primary rounded-xl hover:bg-p
<input type="hidden" id="edit-id" name="id">
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1">Nama Jabatan</label>
<input type="text" id="edit-nama" name="nama_jabatan" required
class="w-full px-4 py-2 border border-slate-300 rounded-xl focus:ring-2 focus:ring-[#130F26] outline-none transition">
</div>
<x-input label="Nama Jabatan" name="nama_jabatan" id="edit-nama" required />
<div class="flex justify-end gap-3 pt-4">
<button type="button" x-data @click="$dispatch('close-modal', 'edit-jabatan')"
@ -95,20 +109,6 @@ class="px-4 py-2 text-sm font-medium text-white bg-amber-500 rounded-xl hover:bg
</form>
</x-modal>
<script>
window.openEditModal = function (btn) {
const id = btn.dataset.id;
const nama = btn.dataset.nama;
document.getElementById('edit-id').value = id;
document.getElementById('edit-nama').value = nama;
let form = document.getElementById('form-edit');
let baseUrl = "{{ route('jabatan.update', ':id') }}";
form.action = baseUrl.replace(':id', id);
window.dispatchEvent(new CustomEvent('open-modal', { detail: 'edit-jabatan' }));
}
</script>
@endsection

View File

@ -13,75 +13,23 @@
<form action="{{ route('jadwal.store') }}" method="POST">
@csrf
<style>
input[type="date"]::-webkit-calendar-picker-indicator,
input[type="date"]::-webkit-inner-spin-button {
display: none;
-webkit-appearance: none;
}
</style>
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.5rem;" class="mb-6">
<div class="space-y-1">
<label class="text-sm font-semibold text-slate-700">Tanggal Mulai</label>
<div class="relative">
<input type="date" name="tanggal_mulai" value="{{ old('tanggal_mulai', now()->format('Y-m-d')) }}" required
onclick="try{this.showPicker()}catch(e){}"
onfocus="try{this.showPicker()}catch(e){}"
class="w-full pl-4 pr-10 py-2.5 bg-white border border-slate-300 rounded-xl text-sm outline-none focus:ring-2 focus:ring-[#130F26] focus:border-[#130F26] transition-all text-slate-600 cursor-pointer appearance-none"
style="-webkit-appearance: none;">
<div class="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none text-slate-500">
<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="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
</svg>
</div>
</div>
@error('tanggal_mulai')
<p class="text-red-500 text-xs mt-1">{{ $message }}</p>
@enderror
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
<x-date-input label="Tanggal Mulai" name="tanggal_mulai"
value="{{ old('tanggal_mulai', now()->format('Y-m-d')) }}" required />
<div class="space-y-1">
<label class="text-sm font-semibold text-slate-700">Tanggal Selesai</label>
<div class="relative">
<input type="date" name="tanggal_selesai" value="{{ old('tanggal_selesai', now()->addDays(6)->format('Y-m-d')) }}" required
onclick="try{this.showPicker()}catch(e){}"
onfocus="try{this.showPicker()}catch(e){}"
class="w-full pl-4 pr-10 py-2.5 bg-white border border-slate-300 rounded-xl text-sm outline-none focus:ring-2 focus:ring-[#130F26] focus:border-[#130F26] transition-all text-slate-600 cursor-pointer appearance-none"
style="-webkit-appearance: none;">
<div class="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none text-slate-500">
<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="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
</svg>
</div>
</div>
@error('tanggal_selesai')
<p class="text-red-500 text-xs mt-1">{{ $message }}</p>
@enderror
</div>
<x-date-input label="Tanggal Selesai" name="tanggal_selesai"
value="{{ old('tanggal_selesai', now()->addDays(6)->format('Y-m-d')) }}" required />
<div class="space-y-1">
<label class="text-sm font-semibold text-slate-700">Shift</label>
<div class="relative">
<select name="id_shift" id="id_shift" required
class="w-full pl-4 pr-10 py-2.5 bg-white border border-slate-300 rounded-xl text-sm outline-none focus:ring-2 focus:ring-[#130F26] focus:border-[#130F26] transition-all text-slate-600 cursor-pointer appearance-none">
<option value="">-- Pilih Shift --</option>
@foreach($shifts as $shift)
<option value="{{ $shift->id_shift }}">{{ $shift->nama_shift }}
({{ \Carbon\Carbon::parse($shift->jam_mulai)->format('H:i') }} -
{{ \Carbon\Carbon::parse($shift->jam_selesai)->format('H:i') }})
</option>
@endforeach
</select>
<div class="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none text-slate-500">
<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="M19 9l-7 7-7-7"></path>
</svg>
</div>
</div>
@error('id_shift')
<p class="text-red-500 text-xs mt-1">{{ $message }}</p>
@enderror
</div>
<x-select label="Shift" name="id_shift" id="id_shift" class="!mb-0" required>
<option value="">-- Pilih Shift --</option>
@foreach($shifts as $shift)
<option value="{{ $shift->id_shift }}" {{ old('id_shift') == $shift->id_shift ? 'selected' : '' }}>
{{ $shift->nama_shift }}
({{ \Carbon\Carbon::parse($shift->jam_mulai)->format('H:i') }} -
{{ \Carbon\Carbon::parse($shift->jam_selesai)->format('H:i') }})
</option>
@endforeach
</x-select>
</div>
<div class="mb-6">

View File

@ -369,7 +369,7 @@ class="text-red-500 text-sm hover:underline font-medium">Hapus Jadwal</button>
</x-modal>
<x-modal name="bulk-delete" title="Hapus Jadwal Massal">
<form action="{{ route('jadwal.bulk-delete') }}" method="POST"
<form action="{{ route('jadwal.bulk-delete') }}" method="POST" id="form-bulk-delete"
x-data="{
tglMulai: '',
tglSelesai: '',
@ -441,7 +441,7 @@ class="user-checkbox peer sr-only">
<div class="flex justify-end gap-3 pt-6 border-t border-slate-100 mt-6">
<x-button type="button" x-data @click="$dispatch('close-modal', 'bulk-delete')" variant="secondary">Batal</x-button>
<x-button type="submit" variant="danger">Ya, Hapus Terpilih</x-button>
<x-button type="button" variant="danger" onclick="confirmBulkDelete()">Ya, Hapus Terpilih</x-button>
</div>
</form>
</x-modal>
@ -469,7 +469,7 @@ function initCalendar() {
window.jadwalCalendar = new FullCalendar.Calendar(calendarEl, {
initialView: 'dayGridMonth',
locale: 'id',
initialDate: '2026-01-01',
initialDate: new Date().toISOString().split('T')[0],
headerToolbar: {
left: 'prev,next today',
center: 'title',
@ -983,40 +983,44 @@ function deleteJadwal() {
}
}
});
selectAllCheckbox.addEventListener('change', function () {
const visibleCheckboxes = document.querySelectorAll('.user-checkbox');
visibleCheckboxes.forEach(cb => cb.checked = this.checked);
});
document.addEventListener('DOMContentLoaded', function() {
const forms = document.querySelectorAll('form');
function confirmBulkDelete() {
const form = document.getElementById('form-bulk-delete');
if (!form) return;
forms.forEach(form => {
const inputMulai = form.querySelector('input[name="tanggal_mulai"]');
const inputSelesai = form.querySelector('input[name="tanggal_selesai"]');
const checkedBoxes = form.querySelectorAll('.user-checkbox:checked');
const tglMulai = form.querySelector('input[name="tanggal_mulai"]')?.value;
const tglSelesai = form.querySelector('input[name="tanggal_selesai"]')?.value;
if (inputMulai && inputSelesai) {
inputMulai.addEventListener('change', function() {
if (this.value) {
inputSelesai.min = this.value;
if (inputSelesai.value && inputSelesai.value < this.value) {
inputSelesai.value = this.value;
}
}
});
inputSelesai.addEventListener('change', function() {
if (this.value) {
inputMulai.max = this.value;
if (inputMulai.value && inputMulai.value > this.value) {
inputMulai.value = this.value;
}
}
});
if (inputMulai.value) inputSelesai.min = inputMulai.value;
if (inputSelesai.value) inputMulai.max = inputSelesai.value;
if (checkedBoxes.length === 0) {
Swal.fire('Peringatan', 'Pilih minimal 1 pegawai untuk dihapus jadwalnya.', 'warning');
return;
}
if (!tglMulai || !tglSelesai) {
Swal.fire('Peringatan', 'Tanggal mulai dan selesai wajib diisi.', 'warning');
return;
}
const formatTgl = (d) => {
const obj = new Date(d);
return obj.toLocaleDateString('id-ID', { day: 'numeric', month: 'long', year: 'numeric' });
};
Swal.fire({
title: 'Konfirmasi Hapus Massal',
html: `Anda akan menghapus jadwal <b>${checkedBoxes.length} pegawai</b> pada rentang:<br><b>${formatTgl(tglMulai)} — ${formatTgl(tglSelesai)}</b><br><br><span class="text-red-500 font-semibold">Tindakan ini tidak dapat dibatalkan!</span>`,
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#ef4444',
cancelButtonColor: '#64748b',
confirmButtonText: 'Ya, Hapus Sekarang',
cancelButtonText: 'Batal'
}).then((result) => {
if (result.isConfirmed) {
form.submit();
}
});
});
}
</script>
@endsection

View File

@ -1,29 +0,0 @@
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='utf-8' />
<script src='https://cdn.jsdelivr.net/npm/fullcalendar@6.1.8/index.global.min.js'></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
var calendarEl = document.getElementById('calendar');
var calendar = new FullCalendar.Calendar(calendarEl, {
initialView: 'dayGridMonth',
initialDate: '2026-01-01',
headerToolbar: {
left: 'prev,next today',
center: 'title',
right: 'dayGridMonth,timeGridWeek,timeGridDay'
}
});
calendar.render();
});
</script>
</head>
<body>
<h1>Test Calendar Isolation</h1>
<div id='calendar'></div>
</body>
</html>

View File

@ -8,7 +8,7 @@
<x-page-header title="Laporan Lembur" subtitle="Rekapitulasi jam dan poin lembur pegawai bulanan.">
<x-slot:actions>
<div class="flex gap-2">
<a href="{{ route('laporan-lembur.exportExcel', ['bulan' => $bulan, 'tahun' => $tahun]) }}" class="inline-flex items-center justify-center px-4 py-2 border border-emerald-600 font-medium rounded-lg text-emerald-700 bg-emerald-50 hover:bg-emerald-100 transition-colors shadow-sm text-sm h-[42px]">
<a href="{{ route('laporan-lembur.exportExcel', ['bulan' => $bulan, 'tahun' => $tahun, 'id_divisi' => $divisiId, 'search' => $search]) }}" class="inline-flex items-center justify-center px-4 py-2 border border-emerald-600 font-medium rounded-lg text-emerald-700 bg-emerald-50 hover:bg-emerald-100 transition-colors shadow-sm text-sm h-[42px]">
<svg class="w-4 h-4 mr-2" 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"></path></svg>
Export Excel
</a>

View File

@ -15,7 +15,7 @@ class="flex flex-wrap gap-4 items-end mb-6 border-b border-slate-100 pb-6">
<x-select label="Bulan" name="bulan" class="!mb-0">
@for ($i = 1; $i <= 12; $i++)
<option value="{{ $i }}" {{ $bulan == $i ? 'selected' : '' }}>
{{ DateTime::createFromFormat('!m', $i)->format('F') }}
{{ \Carbon\Carbon::create()->month($i)->translatedFormat('F') }}
</option>
@endfor
</x-select>
@ -130,7 +130,7 @@ class="inline-flex items-center gap-2 px-4 py-2 bg-red-50 text-red-700 hover:bg-
</td>
</tr>
@empty
<x-empty-state colspan="7" message="Tidak ada data presensi pada periode ini" />
<x-empty-state colspan="8" message="Tidak ada data presensi pada periode ini" />
@endforelse
</x-table>
</div>

View File

@ -2,6 +2,8 @@
@section('title', 'Laporan Izin & Cuti')
@php use App\Enums\StatusPengajuan; @endphp
@section('content')
<div class="space-y-6">
@ -10,6 +12,7 @@
<div class="flex gap-2">
<x-button type="link"
href="{{ route('laporan.izin.export', ['bulan' => $bulan, 'tahun' => $tahun, 'id_divisi' => $divisiId, 'search' => $search]) }}"
data-turbo="false"
class="!bg-emerald-50 !text-emerald-700 hover:!bg-emerald-100 !border-emerald-600 !ring-0 flex items-center gap-2 h-[42px]">
<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"></path></svg>
Export Excel
@ -90,7 +93,7 @@ class="!mb-0" oninput="if(this.value.length === 0) this.form.submit()" />
@if($izin->tanggal_mulai == $izin->tanggal_selesai)
{{ \Carbon\Carbon::parse($izin->tanggal_mulai)->translatedFormat('d M Y') }}
@else
{{ \Carbon\Carbon::parse($izin->tanggal_mulai)->format('d M') }} - {{ \Carbon\Carbon::parse($izin->tanggal_selesai)->translatedFormat('d M Y') }}
{{ \Carbon\Carbon::parse($izin->tanggal_mulai)->translatedFormat('d M') }} - {{ \Carbon\Carbon::parse($izin->tanggal_selesai)->translatedFormat('d M Y') }}
@endif
</td>
<td class="px-6 py-4 text-center whitespace-nowrap">
@ -104,8 +107,8 @@ class="!mb-0" oninput="if(this.value.length === 0) this.form.submit()" />
<td class="px-6 py-4 text-center whitespace-nowrap">
@php
$color = 'yellow'; $label = 'Menunggu';
if($izin->id_status == 2) { $color = 'green'; $label = 'Disetujui'; }
elseif($izin->id_status == 3) { $color = 'red'; $label = 'Ditolak'; }
if($izin->id_status == StatusPengajuan::DISETUJUI) { $color = 'green'; $label = 'Disetujui'; }
elseif($izin->id_status == StatusPengajuan::DITOLAK) { $color = 'red'; $label = 'Ditolak'; }
@endphp
<x-badge color="{{ $color }}">
{{ $label }}

View File

@ -1,4 +1,4 @@
@extends('layouts.app')
@extends('layouts.app')
@section('title', 'Input Lembur Manual')
@ -32,7 +32,7 @@
<option value="">-- Pilih Kompensasi --</option>
@foreach($kompensasi as $k)
<option value="{{ $k->id_kompensasi }}" {{ old('id_kompensasi') == $k->id_kompensasi ? 'selected' : '' }}>
{{ $k->nama_kompensasi }} ({{ $k->id_kompensasi == 2 ? 'Dapat Poin Cut' : 'Uang Lembur' }})
{{ $k->nama_kompensasi }} ({{ $k->id_kompensasi == \App\Enums\JenisKompensasi::TAMBAHAN_POIN ? 'Dapat Poin Cuti' : 'Uang Lembur' }})
</option>
@endforeach
</x-select>

View File

@ -45,7 +45,7 @@
{{ $item->durasi_menit }} Menit
</td>
<td class="px-6 py-4">
@if($item->id_kompensasi == 2)
@if($item->id_kompensasi == \App\Enums\JenisKompensasi::TAMBAHAN_POIN)
<x-badge color="purple"> Poin</x-badge>
@else
<x-badge color="green">💵 Uang</x-badge>
@ -56,12 +56,11 @@
</td>
<td class="px-6 py-4">
@php
$status = strtolower($item->status->nama_status ?? '-');
$idStatus = $item->id_status;
$badgeColor = match ($idStatus) {
1 => 'yellow',
2 => 'green',
3 => 'red',
\App\Enums\StatusPengajuan::PENDING => 'yellow',
\App\Enums\StatusPengajuan::DISETUJUI => 'green',
\App\Enums\StatusPengajuan::DITOLAK => 'red',
default => 'gray'
};
@endphp
@ -70,7 +69,7 @@
</x-badge>
</td>
<td class="px-6 py-4 text-right">
@if($item->id_status == 1)
@if($item->id_status == \App\Enums\StatusPengajuan::PENDING)
<div class="flex justify-end gap-2">
<form action="{{ route('lembur.update', $item->id_lembur) }}" method="POST">

View File

@ -33,7 +33,7 @@ class="grid grid-cols-1 lg:grid-cols-3 gap-6">
required />
</div>
<div class="w-full">
<x-input label="Nomor Telepon" name="no_telp" placeholder="0812..." />
<x-input label="Nomor Telepon" name="no_telp" type="tel" placeholder="0812-XXXX-XXXX" />
</div>
<div class="w-full">
<x-input label="Sisa Cuti (Hari)" name="sisa_cuti" type="number" value="12"
@ -51,7 +51,7 @@ class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<h3 class="text-lg font-bold text-slate-800 mb-4">Penempatan Kerja</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="w-full">
<x-select label="Divisi" name="id_divisi" required>
<x-select label="Divisi" name="id_divisi" class="!mb-0" required>
<option value="">Pilih Divisi</option>
@foreach ($divisi as $d)
<option value="{{ $d->id_divisi }}">{{ $d->nama_divisi }}</option>
@ -60,7 +60,7 @@ class="grid grid-cols-1 lg:grid-cols-3 gap-6">
</div>
<div class="w-full">
<x-select label="Jabatan" name="id_jabatan" required>
<x-select label="Jabatan" name="id_jabatan" class="!mb-0" required>
<option value="">Pilih Jabatan</option>
@foreach ($jabatan as $j)
<option value="{{ $j->id_jabatan }}">{{ $j->nama_jabatan }}</option>
@ -69,7 +69,7 @@ class="grid grid-cols-1 lg:grid-cols-3 gap-6">
</div>
<div class="w-full">
<x-select label="Lokasi Kantor" name="id_kantor" required>
<x-select label="Lokasi Kantor" name="id_kantor" class="!mb-0" required>
<option value="">Pilih Kantor</option>
@foreach ($kantor as $k)
<option value="{{ $k->id_kantor }}">{{ $k->nama_kantor }}</option>
@ -78,7 +78,7 @@ class="grid grid-cols-1 lg:grid-cols-3 gap-6">
</div>
<div class="w-full">
<x-select label="Role Akses" name="id_role" required>
<x-select label="Role Akses" name="id_role" class="!mb-0" required>
<option value="">Pilih Role Akses</option>
@foreach ($roles as $r)
<option value="{{ $r->id_role }}">{{ $r->nama_role }}</option>
@ -86,7 +86,7 @@ class="grid grid-cols-1 lg:grid-cols-3 gap-6">
</x-select>
</div>
<div class="md:col-span-2 w-full mt-2">
<div class="md:col-span-2 w-full">
<x-date-input label="Tanggal Bergabung" name="tgl_bergabung" required />
</div>
</div>

View File

@ -32,7 +32,7 @@ class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<x-input label="Email" name="email" type="email" value="{{ $pegawai->email }}" required />
</div>
<div class="w-full">
<x-input label="Nomor Telepon" name="no_telp" value="{{ $pegawai->no_telp }}" />
<x-input label="Nomor Telepon" name="no_telp" type="tel" value="{{ $pegawai->no_telp }}" placeholder="0812-XXXX-XXXX" />
</div>
<div class="w-full">
<x-input label="Sisa Cuti (Hari)" name="sisa_cuti" type="number" value="{{ $pegawai->sisa_cuti }}"
@ -49,7 +49,7 @@ class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div class="bg-white p-6 rounded-2xl shadow-sm border border-slate-200 block">
<h3 class="text-lg font-bold text-slate-800 mb-4 block w-full">Penempatan & Status</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 w-full">
<x-select label="Divisi" name="id_divisi" required>
<x-select label="Divisi" name="id_divisi" class="!mb-0" required>
@foreach ($divisi as $d)
<option value="{{ $d->id_divisi }}"
{{ $pegawai->id_divisi == $d->id_divisi ? 'selected' : '' }}>
@ -57,7 +57,7 @@ class="grid grid-cols-1 lg:grid-cols-3 gap-6">
@endforeach
</x-select>
<x-select label="Jabatan" name="id_jabatan" required>
<x-select label="Jabatan" name="id_jabatan" class="!mb-0" required>
@foreach ($jabatan as $j)
<option value="{{ $j->id_jabatan }}"
{{ $pegawai->id_jabatan == $j->id_jabatan ? 'selected' : '' }}>{{ $j->nama_jabatan }}
@ -65,7 +65,7 @@ class="grid grid-cols-1 lg:grid-cols-3 gap-6">
@endforeach
</x-select>
<x-select label="Lokasi Kantor" name="id_kantor" required>
<x-select label="Lokasi Kantor" name="id_kantor" class="!mb-0" required>
@foreach ($kantor as $k)
<option value="{{ $k->id_kantor }}"
{{ $pegawai->id_kantor == $k->id_kantor ? 'selected' : '' }}>
@ -73,12 +73,12 @@ class="grid grid-cols-1 lg:grid-cols-3 gap-6">
@endforeach
</x-select>
<x-select label="Status Pegawai" name="status_aktif" required>
<x-select label="Status Pegawai" name="status_aktif" class="!mb-0" required>
<option value="1" {{ $pegawai->status_aktif == 1 ? 'selected' : '' }}>Aktif</option>
<option value="0" {{ $pegawai->status_aktif == 0 ? 'selected' : '' }}>Non-Aktif</option>
</x-select>
<x-select label="Role Akses" name="id_role" required>
<x-select label="Role Akses" name="id_role" class="!mb-0" required>
@foreach ($roles as $r)
<option value="{{ $r->id_role }}"
{{ $pegawai->roles->contains('id_role', $r->id_role) ? 'selected' : '' }}>

View File

@ -254,12 +254,12 @@ class="inline-flex py-1 px-3 rounded-full text-xs font-bold bg-red-100 text-red-
{{ \Carbon\Carbon::parse($presensi->tanggal)->translatedFormat('d M Y') }}
</td>
<td class="px-6 py-4 text-sm text-slate-600 text-center whitespace-nowrap">
{{ $presensi->jadwal->shift->nama_shift ?? 'Shift Default' }}<br>
<span class="text-xs text-slate-400">{{ substr($presensi->jadwal->shift->jam_mulai ?? '08:00', 0, 5) }} - {{ substr($presensi->jadwal->shift->jam_selesai ?? '17:00', 0, 5) }}</span>
{{ $presensi->jadwal?->shift?->nama_shift ?? 'Shift Default' }}<br>
<span class="text-xs text-slate-400">{{ substr($presensi->jadwal?->shift?->jam_mulai ?? '08:00', 0, 5) }} - {{ substr($presensi->jadwal?->shift?->jam_selesai ?? '17:00', 0, 5) }}</span>
</td>
<td class="px-6 py-4 text-center whitespace-nowrap">
<div class="flex flex-col text-sm items-center justify-center">
<span class="font-mono font-bold {{ $presensi->jam_masuk > ($presensi->jadwal->shift->jam_mulai ?? '08:00') ? 'text-red-600' : 'text-emerald-600' }}">
<span class="font-mono font-bold {{ $presensi->jam_masuk > ($presensi->jadwal?->shift?->jam_mulai ?? '08:00') ? 'text-red-600' : 'text-emerald-600' }}">
{{ $presensi->jam_masuk ? \Carbon\Carbon::parse($presensi->jam_masuk)->format('H:i') : '--:--' }}
</span>
<span class="text-xs text-slate-400 font-mono mt-0.5">s/d</span>

View File

@ -1,4 +1,4 @@
@extends('layouts.app')
@extends('layouts.app')
@section('title', 'Penggunaan Poin')
@ -20,33 +20,33 @@
</x-slot:header>
@forelse ($penggunaan as $index => $item)
<tr class="hover:bg-gray-50 transition-colors">
<td class="px-6 py-4 text-sm text-gray-700">
<tr class="hover:bg-slate-50 transition-colors">
<td class="px-6 py-4 text-sm text-slate-700">
{{ $penggunaan->firstItem() + $index }}
</td>
<td class="px-6 py-4">
<div class="text-sm font-medium text-gray-900">{{ $item->user->nama_lengkap ?? '-' }}</div>
<div class="text-xs text-gray-500">{{ $item->user->divisi->nama_divisi ?? '' }}</div>
<div class="text-sm font-medium text-slate-900">{{ $item->user->nama_lengkap ?? '-' }}</div>
<div class="text-xs text-slate-500">{{ $item->user->divisi->nama_divisi ?? '' }}</div>
</td>
<td class="px-6 py-4 text-sm text-gray-700">
<td class="px-6 py-4 text-sm text-slate-700">
{{ $item->tanggal_penggunaan ? $item->tanggal_penggunaan->translatedFormat('d F Y') : '-' }}
</td>
<td class="px-6 py-4 text-sm text-gray-700">
<td class="px-6 py-4 text-sm text-slate-700">
{{ $item->jenisPengurangan->nama_pengurangan ?? 'Lainnya' }}
</td>
<td class="px-6 py-4">
<span class="font-bold text-amber-600">{{ $item->jumlah_poin }} Poin</span>
</td>
<td class="px-6 py-4 text-sm text-gray-500">
<td class="px-6 py-4 text-sm text-slate-500">
{{ $item->tanggal_diajukan ? $item->tanggal_diajukan->translatedFormat('d M Y H:i') : '-' }}
</td>
<td class="px-6 py-4">
@php
$idStatus = $item->id_status;
$badgeColor = match ($idStatus) {
1 => 'yellow',
2 => 'green',
3 => 'red',
\App\Enums\StatusPengajuan::PENDING => 'yellow',
\App\Enums\StatusPengajuan::DISETUJUI => 'green',
\App\Enums\StatusPengajuan::DITOLAK => 'red',
default => 'gray'
};
@endphp
@ -55,7 +55,7 @@
</x-badge>
</td>
<td class="px-6 py-4 text-right">
@if($item->id_status == 1)
@if($item->id_status == \App\Enums\StatusPengajuan::PENDING)
<div class="flex justify-end gap-2">
<form action="{{ route('penggunaan-poin.update', $item->id_penggunaan) }}" method="POST">
@ -65,10 +65,7 @@
<button type="submit"
class="text-green-600 hover:text-green-900 bg-green-50 hover:bg-green-100 p-2 rounded-lg transition-all"
title="Setujui">
<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="M5 13l4 4L19 7"></path>
</svg>
<span class="text-xs font-semibold px-1">Setuju</span>
</button>
</form>
@ -76,14 +73,11 @@ class="text-green-600 hover:text-green-900 bg-green-50 hover:bg-green-100 p-2 ro
onclick="openRejectModal('{{ route('penggunaan-poin.update', $item->id_penggunaan) }}')"
class="text-red-600 hover:text-red-900 bg-red-50 hover:bg-red-100 p-2 rounded-lg transition-all"
title="Tolak">
<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"></path>
</svg>
<span class="text-xs font-semibold px-1">Tolak</span>
</button>
</div>
@else
<span class="text-xs text-gray-400 italic">Selesai</span>
<span class="text-xs text-slate-400 italic">Selesai</span>
@endif
</td>
</tr>
@ -106,7 +100,7 @@ class="text-red-600 hover:text-red-900 bg-red-50 hover:bg-red-100 p-2 rounded-lg
placeholder="Jelaskan alasan kenapa pengajuan ini ditolak..." required rows="3" />
</div>
<div class="flex justify-end gap-3 pt-4 border-t border-gray-100 mt-4">
<div class="flex justify-end gap-3 pt-4 border-t border-slate-100 mt-4">
<x-button type="button" variant="secondary" x-data
@click="$dispatch('close-modal', 'reject-modal')">Batal</x-button>
<x-button type="submit" class="bg-red-600 hover:bg-red-700 text-white">Tolak Pengajuan</x-button>

View File

@ -1,11 +1,13 @@
@extends('layouts.app')
@section('title', 'Buat Pengumuman')
@section('content')
<div class="max-w-2xl mx-auto">
<x-page-header title="Buat Pengumuman Baru" subtitle="Isi form berikut untuk membuat pengumuman" />
<div class="mt-6 bg-white rounded-xl shadow-sm border border-gray-100 p-6">
<div class="mt-6 bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<form action="{{ route('pengumuman.store') }}" method="POST" enctype="multipart/form-data">
@csrf
<div class="space-y-5">

View File

@ -1,11 +1,13 @@
@extends('layouts.app')
@section('title', 'Edit Pengumuman')
@section('content')
<div class="max-w-2xl mx-auto">
<x-page-header title="Edit Pengumuman" subtitle="Perbarui informasi pengumuman" />
<div class="mt-6 bg-white rounded-xl shadow-sm border border-gray-100 p-6">
<div class="mt-6 bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<form action="{{ route('pengumuman.update', $pengumuman->id_pengumuman) }}" method="POST" enctype="multipart/form-data">
@csrf
@method('PUT')

View File

@ -23,13 +23,13 @@
<th class="px-6 py-5 text-xs font-bold text-slate-500 uppercase tracking-wider text-right">Aksi</th>
</x-slot:header>
@forelse ($pengumuman as $item)
<tr class="hover:bg-gray-50 transition-colors">
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<tr class="hover:bg-slate-50 transition-colors">
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-500">
{{ $loop->iteration + ($pengumuman->currentPage() - 1) * $pengumuman->perPage() }}
</td>
<td class="px-6 py-4">
<div class="text-sm font-medium text-gray-900">{{ $item->judul }}</div>
<div class="text-sm text-gray-500 truncate max-w-xs">{{ Str::limit($item->isi, 50) }}</div>
<div class="text-sm font-medium text-slate-900">{{ $item->judul }}</div>
<div class="text-sm text-slate-500 truncate max-w-xs">{{ Str::limit($item->isi, 50) }}</div>
@if($item->foto || $item->lampiran)
<div class="flex gap-2 mt-1">
@if($item->foto)
@ -47,10 +47,10 @@
</div>
@endif
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ $item->tanggal->format('d M Y') }}
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-500">
{{ $item->tanggal ? $item->tanggal->translatedFormat('d M Y') : '-' }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-500">
{{ $item->pembuat->nama_lengkap ?? 'System' }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">

View File

@ -1,5 +1,7 @@
@extends('layouts.app')
@php use App\Enums\StatusValidasi; @endphp
@section('title', 'Monitoring Presensi')
@section('style')
@ -24,7 +26,7 @@
<x-page-header title="Monitoring Presensi" subtitle="Pantau kehadiran pegawai harian.">
<div class="flex gap-2">
@if(Auth::user()->roles->contains(fn($role) => strtolower($role->nama_role) === 'hrd'))
@if(Auth::user()->isGlobalAdmin())
<a href="{{ route('presensi.create') }}" class="inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-lg text-white bg-slate-800 hover:bg-slate-900 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-slate-900 transition-colors shadow-sm text-sm h-[42px]">
<svg class="w-4 h-4 mr-2" 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"></path>
@ -106,9 +108,9 @@ class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-bold
</x-slot:header>
@forelse($presensi as $p)
<tr class="hover:bg-slate-50 border-b border-slate-50 last:border-b-0 {{ $p->id_validasi == 2 ? 'bg-amber-50/50' : '' }}">
<tr class="hover:bg-slate-50 border-b border-slate-50 last:border-b-0 {{ $p->id_validasi == StatusValidasi::PENDING ? 'bg-amber-50/50' : '' }}">
<td class="px-6 py-4">
@if($p->id_validasi == 2)
@if($p->id_validasi == StatusValidasi::PENDING)
<x-checkbox name="presensi_ids[]" value="{{ $p->id_presensi }}" class="presensi-checkbox" />
@endif
</td>
@ -203,7 +205,7 @@ class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-bold
</td>
<td class="px-6 py-4 text-right">
<div class="flex items-center justify-end gap-2">
@if ($p->id_validasi == 2)
@if ($p->id_validasi == StatusValidasi::PENDING)
<button type="button" onclick="inlineApprove({{ $p->id_presensi }})" class="p-1.5 text-emerald-600 hover:text-emerald-800 bg-emerald-50 hover:bg-emerald-100 rounded-lg transition-colors border border-emerald-100" title="Setujui">
<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="M5 13l4 4L19 7"></path></svg>
</button>
@ -212,7 +214,7 @@ class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-bold
</button>
@endif
@if(Auth::user()->roles->contains(fn($role) => strtolower($role->nama_role) === 'hrd'))
@if(Auth::user()->isGlobalAdmin())
<a href="{{ route('presensi.edit', $p->id_presensi) }}" class="p-1.5 text-blue-600 hover:text-blue-800 bg-blue-50 hover:bg-blue-100 rounded-lg transition-colors border border-blue-100" title="Koreksi Jam/Status">
<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.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"></path></svg>
</a>

View File

@ -2,6 +2,40 @@
@section('title', 'Pengaturan Akun')
@section('style')
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.6.2/cropper.min.css">
<style>
.cropper-view-box,
.cropper-face {
border-radius: 50%;
}
.cropper-view-box {
outline: 3px solid rgba(255, 255, 255, 0.8);
outline-offset: 0;
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.5);
}
.cropper-face {
background-color: transparent !important;
}
.cropper-dashed,
.cropper-point,
.cropper-line {
display: none !important;
}
.cropper-crop-box {
border-radius: 50%;
}
.cropper-modal {
background-color: rgba(15, 23, 42, 0.85);
}
</style>
@endsection
@section('content')
<div class="max-w-3xl mx-auto space-y-6">
@ -49,7 +83,7 @@ class="absolute bottom-0 right-0 p-2 bg-white rounded-full shadow-lg border bord
</div>
<div class="text-center">
<h3 class="font-bold text-slate-800">{{ auth()->user()->nama_lengkap }}</h3>
<p class="text-sm text-slate-500">{{ auth()->user()->role->nama_role ?? 'User' }}</p>
<p class="text-sm text-slate-500">{{ auth()->user()->roles->first()->nama_role ?? 'User' }}</p>
</div>
</div>
@ -74,7 +108,7 @@ class="absolute bottom-0 right-0 p-2 bg-white rounded-full shadow-lg border bord
</div>
<div class="flex justify-end pt-6 border-t border-slate-100">
<button type="submit"
<button type="submit" id="submitBtn"
class="px-6 py-2.5 bg-primary text-white font-semibold rounded-xl hover:bg-primary/90 transition shadow-lg shadow-indigo-500/20">
Simpan Perubahan
</button>
@ -165,41 +199,9 @@ class="inline-flex items-center px-5 py-2.5 text-sm font-semibold text-white bg-
</div>
</div>
</div>
@endsection
{{-- Cropper.js CDN --}}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.6.2/cropper.min.css">
@section('script')
<script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.6.2/cropper.min.js"></script>
<script src="{{ asset('js/image-cropper.js') }}"></script>
{{-- Cropper circular overlay --}}
<style>
.cropper-view-box,
.cropper-face {
border-radius: 50%;
}
.cropper-view-box {
outline: 3px solid rgba(255, 255, 255, 0.8);
outline-offset: 0;
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.5);
}
.cropper-face {
background-color: transparent !important;
}
.cropper-dashed,
.cropper-point,
.cropper-line {
display: none !important;
}
.cropper-crop-box {
border-radius: 50%;
}
.cropper-modal {
background-color: rgba(15, 23, 42, 0.85);
}
</style>
@endsection

View File

@ -37,7 +37,7 @@ class="flex items-center gap-2 px-4 py-2 bg-primary text-white rounded-lg hover:
@forelse($roles as $index => $role)
@php
$roleKritis = in_array(strtolower($role->nama_role), ['manajer', 'manager', 'supervisor', 'hrd', 'super_admin', 'staff']);
$roleKritis = in_array(strtolower($role->nama_role), \App\Http\Controllers\RoleController::ROLE_KRITIS);
@endphp
<tr class="hover:bg-slate-50 transition border-b border-slate-50 last:border-b-0">
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-600">{{ $roles->firstItem() + $index }}</td>

View File

@ -1,4 +1,4 @@
@extends('layouts.app')
@extends('layouts.app')
@section('title', 'Detail Surat Izin')
@ -191,29 +191,25 @@
</div>
@endif
</div>
@section('style')
<style>
@media print {
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
/* Sembunyikan elemen layout umum (sidebar & header) */
aside, header {
display: none !important;
}
/* Reset constraint agar browser dapat memproses multi-halaman jika suratnya panjang */
body, html, #main-scroll-container, main, .h-screen, .overflow-hidden, .overflow-y-auto {
height: auto !important;
min-height: auto !important;
overflow: visible !important;
background-color: transparent !important;
margin: 0 !important;
padding: 0 !important;
position: static !important;
}
@page { margin: 1.5cm; }
}
</style>
@endsection
@endsection
@section('style')
<style>
@media print {
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
aside, header {
display: none !important;
}
body, html, #main-scroll-container, main, .h-screen, .overflow-hidden, .overflow-y-auto {
height: auto !important;
min-height: auto !important;
overflow: visible !important;
background-color: transparent !important;
margin: 0 !important;
padding: 0 !important;
position: static !important;
}
@page { margin: 1.5cm; }
}
</style>
@endsection

View File

@ -1,4 +1,4 @@
@extends('layouts.app')
@extends('layouts.app')
@section('title', 'Tukar Shift Kerja')
@ -190,7 +190,7 @@ function initTukarShiftForm() {
form.addEventListener('submit', function (e) {
if (user1Select.value === user2Select.value && user1Select.value !== '') {
e.preventDefault();
alert('Pegawai pertama dan kedua tidak boleh orang yang sama!');
Swal.fire('Peringatan', 'Pegawai pertama dan kedua tidak boleh orang yang sama!', 'warning');
}
});
}
@ -280,7 +280,7 @@ function filterAllPegawaiByKantor() {
} catch (error) {
console.error('Error fetching schedules:', error);
targetSelect.innerHTML = '<option value="">Gagal mengambil data</option>';
alert('Terjadi kesalahan saat mengambil jadwal pegawai: ' + error.message);
Swal.fire('Gagal', 'Terjadi kesalahan saat mengambil jadwal pegawai: ' + error.message, 'error');
} finally {
if (loadingEl) { loadingEl.classList.add('hidden'); }
if (arrowEl) { arrowEl.classList.remove('hidden'); }
@ -319,5 +319,6 @@ function filterAllPegawaiByKantor() {
document.addEventListener('DOMContentLoaded', initTukarShiftForm);
}
</script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script src="{{ asset('js/form-handler.js') }}"></script>
@endsection