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

View File

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

View File

@ -258,8 +258,15 @@ public function history(Request $request)
try { try {
$masuk = Carbon::parse($item->tanggal . ' ' . $item->jam_masuk); $masuk = Carbon::parse($item->tanggal . ' ' . $item->jam_masuk);
$pulang = Carbon::parse($item->tanggal . ' ' . $item->jam_pulang); $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) { } catch (\Exception $e) {
$totalJam = '-'; $totalJam = '-';
} }

View File

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

View File

@ -13,6 +13,8 @@
use App\Models\PenggunaanPoin; use App\Models\PenggunaanPoin;
use App\Services\PoinService; use App\Services\PoinService;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use App\Http\Requests\StoreJadwalRequest;
use App\Http\Requests\BulkDestroyJadwalRequest;
class JadwalController extends Controller class JadwalController extends Controller
{ {
@ -28,7 +30,7 @@ public function index()
return $s; return $s;
}); });
$pegawaiQuery = User::where('status_aktif', 1); $pegawaiQuery = User::where('status_aktif', 1)->bukanHrd();
if (!$isGlobalAdmin) { if (!$isGlobalAdmin) {
$pegawaiQuery->where('id_kantor', $user->id_kantor); $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(); $kantor = $isGlobalAdmin ? Kantor::all() : Kantor::where('id_kantor', $user->id_kantor)->get();
$divisi = Divisi::all(); $divisi = Divisi::all();
$pegawaiQuery = User::where('status_aktif', 1)->with(['kantor', 'jabatan']); $pegawaiQuery = User::where('status_aktif', 1)->bukanHrd()->with(['kantor', 'jabatan']);
if (!$isGlobalAdmin) { if (!$isGlobalAdmin) {
$pegawaiQuery->where('id_kantor', $user->id_kantor); $pegawaiQuery->where('id_kantor', $user->id_kantor);
} }
@ -220,15 +222,9 @@ public function create()
return view('jadwal.create', compact('shifts', 'pegawai', 'kantor', 'divisi')); return view('jadwal.create', compact('shifts', 'pegawai', 'kantor', 'divisi'));
} }
public function store(Request $request) public function store(StoreJadwalRequest $request)
{ {
$request->validate([ $data = $request->validated();
'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',
]);
// Guard: pastikan ada data shift sebelum memproses // Guard: pastikan ada data shift sebelum memproses
if (ShiftKerja::count() === 0) { if (ShiftKerja::count() === 0) {
@ -249,12 +245,12 @@ public function store(Request $request)
} }
} }
$startDate = Carbon::parse($request->tanggal_mulai); $startDate = Carbon::parse($data['tanggal_mulai']);
$endDate = Carbon::parse($request->tanggal_selesai); $endDate = Carbon::parse($data['tanggal_selesai']);
$period = \Carbon\CarbonPeriod::create($startDate, $endDate); $period = \Carbon\CarbonPeriod::create($startDate, $endDate);
$shiftId = $request->id_shift; $shiftId = $data['id_shift'];
$userIds = $request->user_ids; $userIds = $data['user_ids'];
$count = 0; $count = 0;
$refundCount = 0; $refundCount = 0;
@ -364,38 +360,33 @@ public function destroy($id)
return response()->json(['success' => true, 'message' => 'Jadwal berhasil dihapus']); return response()->json(['success' => true, 'message' => 'Jadwal berhasil dihapus']);
} }
public function bulkDestroy(Request $request) public function bulkDestroy(BulkDestroyJadwalRequest $request)
{ {
$request->validate([ $data = $request->validated();
'tanggal_mulai' => 'required|date',
'tanggal_selesai' => 'required|date|after_or_equal:tanggal_mulai',
'user_ids' => 'required|array',
'user_ids.*' => 'exists:users,id',
]);
$user = Auth::user(); $user = Auth::user();
$isGlobalAdmin = $user->isGlobalAdmin(); $isGlobalAdmin = $user->isGlobalAdmin();
if (!$isGlobalAdmin) { if (!$isGlobalAdmin) {
$invalidUsers = User::whereIn('id', $request->user_ids) $invalidUsers = User::whereIn('id', $data['user_ids'])
->where('id_kantor', '!=', $user->id_kantor) ->where('id_kantor', '!=', $user->id_kantor)
->exists(); ->exists();
if ($invalidUsers) { 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 { try {
DB::transaction(function () use ($request) { DB::transaction(function () use ($data) {
JadwalKerja::whereIn('id_user', $request->user_ids) JadwalKerja::whereIn('id_user', $data['user_ids'])
->whereBetween('tanggal', [$request->tanggal_mulai, $request->tanggal_selesai]) ->whereBetween('tanggal', [$data['tanggal_mulai'], $data['tanggal_selesai']])
->delete(); ->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) { } 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\User;
use App\Models\Presensi; use App\Models\Presensi;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Carbon\Carbon; use Carbon\Carbon;
use Maatwebsite\Excel\Facades\Excel; use Maatwebsite\Excel\Facades\Excel;
@ -17,11 +18,15 @@ class LaporanController extends Controller
{ {
public function index(Request $request) public function index(Request $request)
{ {
$bulan = $request->input('bulan', date('m')); $bulan = $request->input('bulan', date('m'));
$tahun = $request->input('tahun', date('Y')); $tahun = $request->input('tahun', date('Y'));
$divisiId = $request->input('id_divisi'); $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(); $divisiList = \App\Models\Divisi::all();
return view('laporan.index', compact('rekap', 'bulan', 'tahun', 'divisiList', 'divisiId')); return view('laporan.index', compact('rekap', 'bulan', 'tahun', 'divisiList', 'divisiId'));
@ -29,15 +34,24 @@ public function index(Request $request)
public function cuti(Request $request) public function cuti(Request $request)
{ {
$bulan = $request->input('bulan', date('m')); $bulan = $request->input('bulan', date('m'));
$tahun = $request->input('tahun', date('Y')); $tahun = $request->input('tahun', date('Y'));
$search = $request->input('search'); $search = $request->input('search');
$divisiId = $request->input('id_divisi'); $divisiId = $request->input('id_divisi');
$authUser = Auth::user();
$isGlobalAdmin = $authUser->isGlobalAdmin();
$query = \App\Models\PengajuanIzin::with(['user.divisi', 'jenisIzin']) $query = \App\Models\PengajuanIzin::with(['user.divisi', 'jenisIzin'])
->whereYear('tanggal_mulai', $tahun) ->whereYear('tanggal_mulai', $tahun)
->whereMonth('tanggal_mulai', $bulan); ->whereMonth('tanggal_mulai', $bulan);
if (!$isGlobalAdmin) {
$query->whereHas('user', function ($q) use ($authUser) {
$q->where('id_kantor', $authUser->id_kantor);
});
}
if ($search) { if ($search) {
$query->whereHas('user', function($q) use ($search) { $query->whereHas('user', function($q) use ($search) {
$q->where('nama_lengkap', 'like', "%{$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(); $divisiList = \App\Models\Divisi::all();
return view('laporan.izin', compact('izinList', 'bulan', 'tahun', 'search', 'divisiId', 'divisiList')); return view('laporan.izin', compact('izinList', 'bulan', 'tahun', 'search', 'divisiId', 'divisiList'));
@ -64,10 +78,19 @@ public function exportIzinExcel(Request $request)
$search = $request->input('search'); $search = $request->input('search');
$divisiId = $request->input('id_divisi'); $divisiId = $request->input('id_divisi');
$authUser = Auth::user();
$isGlobalAdmin = $authUser->isGlobalAdmin();
$query = \App\Models\PengajuanIzin::with(['user.divisi', 'jenisIzin']) $query = \App\Models\PengajuanIzin::with(['user.divisi', 'jenisIzin'])
->whereYear('tanggal_mulai', $tahun) ->whereYear('tanggal_mulai', $tahun)
->whereMonth('tanggal_mulai', $bulan); ->whereMonth('tanggal_mulai', $bulan);
if (!$isGlobalAdmin) {
$query->whereHas('user', function ($q) use ($authUser) {
$q->where('id_kantor', $authUser->id_kantor);
});
}
if ($search) { if ($search) {
$query->whereHas('user', function ($q) use ($search) { $query->whereHas('user', function ($q) use ($search) {
$q->where('nama_lengkap', 'like', "%{$search}%") $q->where('nama_lengkap', 'like', "%{$search}%")
@ -94,11 +117,14 @@ public function exportIzinExcel(Request $request)
public function exportExcel(Request $request) public function exportExcel(Request $request)
{ {
$bulan = $request->input('bulan', date('m')); $bulan = $request->input('bulan', date('m'));
$tahun = $request->input('tahun', date('Y')); $tahun = $request->input('tahun', date('Y'));
$divisiId = $request->input('id_divisi'); $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)) { if (empty($rekap)) {
return redirect()->back()->with('error', 'Tidak ada data presensi untuk diekspor pada periode tersebut.'); 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) public function exportPdf(Request $request)
{ {
$bulan = $request->input('bulan', date('m')); $bulan = $request->input('bulan', date('m'));
$tahun = $request->input('tahun', date('Y')); $tahun = $request->input('tahun', date('Y'));
$divisiId = $request->input('id_divisi'); $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 = Pdf::loadView('laporan.pdf', compact('rekap', 'bulan', 'tahun'));
$pdf->setPaper('a4', 'landscape'); $pdf->setPaper('a4', 'landscape');
@ -123,19 +152,24 @@ public function exportPdf(Request $request)
return $pdf->download("Laporan_Presensi_{$bulan}_{$tahun}.pdf"); 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; $idTepatWaktu = StatusPresensi::TEPAT_WAKTU;
$idTerlambat = StatusPresensi::TERLAMBAT; $idTerlambat = StatusPresensi::TERLAMBAT;
$idIzin = StatusPresensi::IZIN; $idIzin = StatusPresensi::IZIN;
$idSakit = StatusPresensi::SAKIT; $idSakit = StatusPresensi::SAKIT;
$idAlpha = StatusPresensi::ALPHA; $idAlpha = StatusPresensi::ALPHA;
$pegawaiQuery = User::with(['jabatan', 'divisi']) $pegawaiQuery = User::with(['jabatan', 'divisi'])
->where('status_aktif', 1) ->where('status_aktif', 1)
->bukanHrd()
->select('id', 'nama_lengkap', 'nik', 'id_jabatan', 'id_divisi') ->select('id', 'nama_lengkap', 'nik', 'id_jabatan', 'id_divisi')
->orderBy('nama_lengkap', 'asc'); ->orderBy('nama_lengkap', 'asc');
if ($kantorId) {
$pegawaiQuery->where('id_kantor', $kantorId);
}
if ($divisiId) { if ($divisiId) {
$pegawaiQuery->where('id_divisi', $divisiId); $pegawaiQuery->where('id_divisi', $divisiId);
} }

View File

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

View File

@ -10,6 +10,7 @@
use App\Services\LemburService; use App\Services\LemburService;
use App\Services\NotifikasiService; use App\Services\NotifikasiService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
class LemburController extends Controller class LemburController extends Controller
@ -23,10 +24,10 @@ public function __construct(LemburService $lemburService)
public function index() public function index()
{ {
$user = \Illuminate\Support\Facades\Auth::user(); $user = Auth::user();
$isGlobalAdmin = $user->isGlobalAdmin(); $isGlobalAdmin = $user->isGlobalAdmin();
$query = Lembur::with(['user', 'status', 'kompensasi']); $query = Lembur::with(['user.jabatan', 'status', 'kompensasi']);
if (!$isGlobalAdmin) { if (!$isGlobalAdmin) {
$query->whereHas('user', function ($q) use ($user) { $query->whereHas('user', function ($q) use ($user) {
@ -43,7 +44,14 @@ public function index()
public function create() 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(); $kompensasi = JenisKompensasi::all();
return view('lembur.create', compact('pegawai', 'kompensasi')); return view('lembur.create', compact('pegawai', 'kompensasi'));
} }
@ -53,19 +61,40 @@ public function store(Request $request)
$request->validate([ $request->validate([
'id_user' => 'required|exists:users,id', 'id_user' => 'required|exists:users,id',
'tanggal_lembur' => 'required|date', 'tanggal_lembur' => 'required|date',
'jam_mulai' => 'required', 'jam_mulai' => 'required|date_format:H:i',
'jam_selesai' => 'required', 'jam_selesai' => 'required|date_format:H:i|different:jam_mulai',
'id_kompensasi' => 'required|exists:jenis_kompensasi,id_kompensasi', '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 { try {
DB::transaction(function () use ($request) { $lembur = null;
$user = User::findOrFail($request->id_user); DB::transaction(function () use ($request, $targetUser, &$lembur) {
$lembur = $this->lemburService->createLembur($user, $request->all()); $lembur = $this->lemburService->createLembur($targetUser, $request->all());
$this->lemburService->approve($lembur); $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.'); return redirect()->route('lembur.index')->with('success', 'Data lembur manual berhasil ditambahkan dan disetujui otomatis.');
} catch (\Exception $e) { } catch (\Exception $e) {
return redirect()->back()->with('error', 'Gagal menambahkan lembur: ' . $e->getMessage())->withInput(); return redirect()->back()->with('error', 'Gagal menambahkan lembur: ' . $e->getMessage())->withInput();
@ -76,11 +105,11 @@ public function update(Request $request, Lembur $lembur)
{ {
$request->validate([ $request->validate([
'action' => 'required|in:approve,reject', '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'); $lembur->load('user');
$userAuth = \Illuminate\Support\Facades\Auth::user(); $userAuth = Auth::user();
if (!$userAuth->isGlobalAdmin() && $lembur->user->id_kantor != $userAuth->id_kantor) { if (!$userAuth->isGlobalAdmin() && $lembur->user->id_kantor != $userAuth->id_kantor) {
return redirect()->back()->with('error', 'Anda tidak diizinkan memproses data dari kantor lain.'); 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) public function index(Request $request)
{ {
$query = User::with(['divisi', 'jabatan', 'kantor', 'roles']) $query = User::with(['divisi', 'jabatan', 'kantor', 'roles'])->bukanHrd();
->whereDoesntHave('roles', function ($q) {
$q->where('nama_role', 'hrd');
});
if ($request->filled('filter_jabatan')) { if ($request->filled('filter_jabatan')) {
$query->where('id_jabatan', $request->filter_jabatan); $query->where('id_jabatan', $request->filter_jabatan);
@ -48,21 +45,14 @@ public function index(Request $request)
$allKantor = Kantor::withCount([ $allKantor = Kantor::withCount([
'users as total_pegawai' => function ($q) { 'users as total_pegawai' => function ($q) {
$q->whereDoesntHave('roles', function ($sq) { $q->bukanHrd();
$sq->where('nama_role', 'hrd');
});
} }
])->get(); ])->get();
$stats = [ $stats = [
'kantor_list' => $allKantor, 'kantor_list' => $allKantor,
'total' => User::whereDoesntHave('roles', function ($q) { 'total' => User::bukanHrd()->count(),
$q->where('nama_role', 'hrd'); 'active' => User::bukanHrd()->where('status_aktif', 1)->count(),
})->count(),
'active' => User::where('status_aktif', 1)
->whereDoesntHave('roles', function ($q) {
$q->where('nama_role', 'hrd');
})->count(),
]; ];
$allJabatan = Jabatan::all(); $allJabatan = Jabatan::all();

View File

@ -7,8 +7,13 @@
use App\Enums\StatusPresensi; use App\Enums\StatusPresensi;
use App\Enums\StatusSurat; use App\Enums\StatusSurat;
use App\Enums\StatusValidasi; 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\PengajuanIzin;
use App\Models\Presensi; use App\Models\Presensi;
use App\Models\StatusPengajuan as StatusPengajuanModel;
use App\Models\User; use App\Models\User;
use App\Models\SuratIzin; use App\Models\SuratIzin;
use App\Models\TandaTangan; use App\Models\TandaTangan;
@ -47,28 +52,31 @@ public function index(Request $request)
$izin = $query->orderBy('created_at', 'desc')->paginate(15); $izin = $query->orderBy('created_at', 'desc')->paginate(15);
$statuses = \App\Models\StatusPengajuan::all(); $statuses = StatusPengajuanModel::orderBy('id_status')->get();
$jenisIzin = \App\Models\JenisIzin::all(); $jenisIzinList = 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();
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); $jenisIzinData = JenisIzin::find($request->id_jenis_izin);
if ($jenisIzin && $jenisIzin->nama_izin == 'Cuti') { if ($jenisIzinData && $jenisIzinData->id_jenis_izin == JenisIzinEnum::CUTI) {
$pegawai = User::find($request->id_user); $pegawai = User::find($request->id_user);
$tanggalMulai = Carbon::parse($request->tanggal_mulai); $tanggalMulai = Carbon::parse($request->tanggal_mulai);
$tanggalSelesai = Carbon::parse($request->tanggal_selesai); $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); $tanggalMulai = Carbon::parse($request->tanggal_mulai);
$jumlahHari = $tanggalMulai->diffInDays(Carbon::parse($request->tanggal_selesai)) + 1; $jumlahHari = $tanggalMulai->diffInDays(Carbon::parse($request->tanggal_selesai)) + 1;
$sudahSakitBulanIni = PengajuanIzin::where('id_user', $request->id_user) $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]) ->whereIn('id_status', [StatusPengajuan::PENDING, StatusPengajuan::DISETUJUI])
->whereMonth('tanggal_mulai', $tanggalMulai->month) ->whereMonth('tanggal_mulai', $tanggalMulai->month)
->whereYear('tanggal_mulai', $tanggalMulai->year) ->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!'); 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]) ->whereBetween('tanggal', [$tglMulai, $tglSelesai])
->whereNotNull('jam_masuk') ->whereNotNull('jam_masuk')
->exists(); ->exists();
@ -260,10 +268,9 @@ public function approve($id)
public function reject(Request $request, $id) public function reject(Request $request, $id)
{ {
$izin = PengajuanIzin::with('user')->findOrFail($id); $izin = PengajuanIzin::with(['user', 'jenisIzin'])->findOrFail($id);
$user = Auth::user(); $user = Auth::user();
// Security Check
if (!$user->isGlobalAdmin() && $izin->user->id_kantor != $user->id_kantor) { if (!$user->isGlobalAdmin() && $izin->user->id_kantor != $user->id_kantor) {
return redirect()->back()->with('error', 'Anda tidak diizinkan menolak pengajuan dari kantor lain.'); 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.'); return redirect()->back()->with('error', 'Pengajuan Cuti harus ditolak melalui menu Surat Izin.');
} }
$izin->update([ try {
'id_status' => StatusPengajuan::DITOLAK DB::beginTransaction();
]);
app(NotifikasiService::class)->kirim( $izin->update([
$izin->id_user, 'id_status' => StatusPengajuan::DITOLAK,
'izin_ditolak', 'alasan_penolakan' => $request->alasan_penolakan,
'Pengajuan Izin Ditolak', ]);
'Pengajuan ' . ($izin->jenisIzin->nama_izin ?? 'Izin') . ' Anda ditolak.',
['id_izin' => $izin->id_izin]
);
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\PoinService;
use App\Services\NotifikasiService; use App\Services\NotifikasiService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
class PenggunaanPoinController extends Controller class PenggunaanPoinController extends Controller
@ -20,10 +21,10 @@ public function __construct(PoinService $poinService)
public function index() public function index()
{ {
$user = \Illuminate\Support\Facades\Auth::user(); $user = Auth::user();
$isGlobalAdmin = $user->isGlobalAdmin(); $isGlobalAdmin = $user->isGlobalAdmin();
$query = PenggunaanPoin::with(['user', 'jenisPengurangan', 'status']); $query = PenggunaanPoin::with(['user.divisi', 'jenisPengurangan', 'status']);
if (!$isGlobalAdmin) { if (!$isGlobalAdmin) {
$query->whereHas('user', function ($q) use ($user) { $query->whereHas('user', function ($q) use ($user) {
@ -48,7 +49,7 @@ public function update(Request $request, $id)
$penggunaan = PenggunaanPoin::findOrFail($id); $penggunaan = PenggunaanPoin::findOrFail($id);
$penggunaan->load('user'); $penggunaan->load('user');
$userAuth = \Illuminate\Support\Facades\Auth::user(); $userAuth = Auth::user();
if (!$userAuth->isGlobalAdmin() && $penggunaan->user->id_kantor != $userAuth->id_kantor) { if (!$userAuth->isGlobalAdmin() && $penggunaan->user->id_kantor != $userAuth->id_kantor) {
return redirect()->back()->with('error', 'Anda tidak diizinkan memproses data dari kantor lain.'); 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, $penggunaan->id_user,
'poin_disetujui', 'poin_disetujui',
'Penggunaan 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] ['id_penggunaan' => $penggunaan->id_penggunaan]
); );
@ -103,7 +104,7 @@ public function update(Request $request, $id)
$penggunaan->id_user, $penggunaan->id_user,
'poin_ditolak', 'poin_ditolak',
'Penggunaan 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] ['id_penggunaan' => $penggunaan->id_penggunaan]
); );

View File

@ -47,7 +47,7 @@ public function store(StorePengumumanRequest $request)
'pengumuman_baru', 'pengumuman_baru',
'📢 Pengumuman Baru', '📢 Pengumuman Baru',
$pengumuman->judul, $pengumuman->judul,
['id_pengumuman' => $pengumuman->id] ['id_pengumuman' => $pengumuman->id_pengumuman]
); );
return redirect()->route('pengumuman.index') return redirect()->route('pengumuman.index')
@ -86,16 +86,20 @@ public function update(UpdatePengumumanRequest $request, Pengumuman $pengumuman)
public function destroy(Pengumuman $pengumuman) public function destroy(Pengumuman $pengumuman)
{ {
if ($pengumuman->foto) { try {
Storage::disk('public')->delete($pengumuman->foto); if ($pengumuman->foto) {
} Storage::disk('public')->delete($pengumuman->foto);
if ($pengumuman->lampiran) { }
Storage::disk('public')->delete($pengumuman->lampiran); if ($pengumuman->lampiran) {
} Storage::disk('public')->delete($pengumuman->lampiran);
}
$pengumuman->delete(); $pengumuman->delete();
return redirect()->route('pengumuman.index') return redirect()->route('pengumuman.index')
->with('success', 'Pengumuman berhasil dihapus.'); ->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 App\Http\Requests\StoreManualPresensiRequest;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str; use Illuminate\Support\Str;
@ -228,6 +229,7 @@ public function create()
$isGlobalAdmin = $user->isGlobalAdmin(); $isGlobalAdmin = $user->isGlobalAdmin();
$pegawaiQuery = \App\Models\User::with('divisi') $pegawaiQuery = \App\Models\User::with('divisi')
->bukanHrd()
->orderBy('nama_lengkap', 'asc'); ->orderBy('nama_lengkap', 'asc');
if (!$isGlobalAdmin) { if (!$isGlobalAdmin) {
@ -312,7 +314,7 @@ public function updateManual(Request $request, $id)
'alasan_telat' => 'required|string|max:255' 'alasan_telat' => 'required|string|max:255'
], [ ], [
'alasan_telat.required' => 'Catatan koreksi wajib diisi sebagai dokumentasi log.', '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); $presensi = Presensi::findOrFail($id);
@ -425,7 +427,7 @@ public function store(StorePresensiRequest $request)
$faceMessage = null; $faceMessage = null;
try { try {
$faceService = new \App\Services\FaceRecognitionService(); $faceService = app(\App\Services\FaceRecognitionService::class);
$result = $faceService->verifyFace($user->id, $fotoFile); $result = $faceService->verifyFace($user->id, $fotoFile);
if (isset($result['verified']) && $result['verified'] === true) { if (isset($result['verified']) && $result['verified'] === true) {
@ -434,6 +436,10 @@ public function store(StorePresensiRequest $request)
$faceMessage = $result['message'] ?? null; $faceMessage = $result['message'] ?? null;
} catch (\Exception $e) { } catch (\Exception $e) {
Log::warning('Verifikasi wajah masuk gagal', [
'user_id' => $user->id,
'error' => $e->getMessage(),
]);
} }
$fotoFile->storeAs('uploads/absensi', $imageName, 'public'); $fotoFile->storeAs('uploads/absensi', $imageName, 'public');
@ -486,12 +492,16 @@ private function handlePulang($request, $user, $hariIni, $jamSekarang)
$verifikasiPulang = 0; $verifikasiPulang = 0;
try { try {
$faceService = new \App\Services\FaceRecognitionService(); $faceService = app(\App\Services\FaceRecognitionService::class);
$result = $faceService->verifyFace($user->id, $fotoFile); $result = $faceService->verifyFace($user->id, $fotoFile);
if (isset($result['verified']) && $result['verified'] === true) { if (isset($result['verified']) && $result['verified'] === true) {
$verifikasiPulang = 1; $verifikasiPulang = 1;
} }
} catch (\Exception $e) { } catch (\Exception $e) {
Log::warning('Verifikasi wajah pulang gagal', [
'user_id' => $user->id,
'error' => $e->getMessage(),
]);
} }
$fotoFile->storeAs('uploads/absensi', $imageName, 'public'); $fotoFile->storeAs('uploads/absensi', $imageName, 'public');

View File

@ -2,34 +2,25 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use Illuminate\Http\Request; use App\Http\Requests\UpdateProfileRequest;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\Rule;
class ProfileController extends Controller class ProfileController extends Controller
{ {
public function edit() public function edit()
{ {
$user = Auth::user(); return view('profile.edit');
return view('profile.edit', compact('user'));
} }
public function update(Request $request) public function update(UpdateProfileRequest $request)
{ {
$user = Auth::user(); $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 = [ $data = [
'nama_lengkap' => $request->nama_lengkap, 'nama_lengkap' => $request->nama_lengkap,
'email' => $request->email, 'email' => $request->email,
]; ];
if ($request->filled('password')) { if ($request->filled('password')) {

View File

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

View File

@ -18,18 +18,26 @@ class TukarShiftController extends Controller
public function index() public function index()
{ {
$riwayat = RiwayatTukarShift::with(['user1', 'user2', 'jadwal1.shift', 'jadwal2.shift', 'execAdmin']) $user = Auth::user();
->latest()
->get(); $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')); return view('tukar-shift.index', compact('riwayat'));
} }
public function create() public function create()
{ {
$pegawai = User::with('kantor')->whereDoesntHave('roles', function ($q) { $pegawai = User::with('kantor')->bukanHrd()->get();
$q->where('nama_role', 'hrd');
})->get();
$kantor = \App\Models\Kantor::orderBy('nama_kantor', 'asc')->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 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 public function rules(): array
{ {
return [ 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'], 'nama_lengkap' => ['required', 'string', 'max:255'],
'nik' => ['nullable', 'string', 'max:50', 'unique:users,nik'], 'nik' => ['nullable', 'string', 'max:50', 'unique:users,nik'],
'email' => ['required', 'email', 'unique:users,email'], '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'], 'alamat' => ['nullable', 'string'],
'id_divisi' => ['required', 'exists:divisi,id_divisi'], 'id_divisi' => ['required', 'exists:divisi,id_divisi'],
'id_jabatan' => ['required', 'exists:jabatan,id_jabatan'], 'id_jabatan' => ['required', 'exists:jabatan,id_jabatan'],
@ -32,10 +33,13 @@ public function rules(): array
public function messages(): array public function messages(): array
{ {
return [ return [
'nik.unique' => 'NIK sudah terdaftar di sistem.', 'nik.unique' => 'NIK sudah terdaftar di sistem.',
'email.unique' => 'Email ini sudah digunakan oleh pegawai lain.', 'email.unique' => 'Email ini sudah digunakan oleh pegawai lain.',
'id_divisi.exists' => 'Divisi yang dipilih tidak valid.', '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 public function rules(): array
{ {
return [ return [
'tanggal_mulai' => 'required|date|after_or_equal:today', 'id_user' => 'required|exists:users,id',
'tanggal_selesai' => 'required|date|after_or_equal:tanggal_mulai', 'id_jenis_izin' => 'required|exists:jenis_izin,id_jenis_izin',
'id_jenis_izin' => 'required|exists:jenis_izin,id_jenis_izin', 'tanggal_mulai' => 'required|date',
'alasan' => 'required|string|max:150', 'tanggal_selesai'=> 'required|date|after_or_equal:tanggal_mulai',
'bukti_file' => 'nullable|file|mimes:pdf,jpg,jpeg,png|max:2048', '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() public function attributes()
{ {
return [ return [
'tanggal_mulai' => 'tanggal mulai', 'id_user' => 'pegawai',
'id_jenis_izin' => 'jenis izin',
'tanggal_mulai' => 'tanggal mulai',
'tanggal_selesai' => 'tanggal selesai', 'tanggal_selesai' => 'tanggal selesai',
'id_jenis_izin' => 'jenis izin', 'alasan' => 'alasan',
'bukti_file' => 'bukti file', 'bukti_file' => 'bukti file',
]; ];
} }

View File

@ -22,18 +22,34 @@ public function rules(): array
'email' => [ 'email' => [
'required', 'required',
'email', 'email',
'unique:users,email,' . $this->route('pegawai') Rule::unique('users', 'email')->ignore($id),
], ],
'no_telp' => ['nullable', 'string', 'max:20'], 'no_telp' => ['nullable', 'string', 'max:15', 'regex:/^(\+62|62|0)[0-9]{8,11}$/'],
'alamat' => ['nullable', 'string'], 'alamat' => ['nullable', 'string'],
'id_divisi' => ['required', 'exists:divisi,id_divisi'], 'id_divisi' => ['required', 'exists:divisi,id_divisi'],
'id_jabatan' => ['required', 'exists:jabatan,id_jabatan'], 'id_jabatan' => ['required', 'exists:jabatan,id_jabatan'],
'id_kantor' => ['required', 'exists:kantor,id_kantor'], 'id_kantor' => ['required', 'exists:kantor,id_kantor'],
'status_aktif' => ['required', 'in:0,1'], 'status_aktif' => ['required', 'in:0,1'],
'tgl_bergabung' => ['required', 'date'], 'tgl_bergabung' => ['required', 'date'],
'foto' => ['nullable', 'image', 'max:5120'], 'foto' => ['nullable', 'image', 'max:5120'],
'sisa_cuti' => ['nullable', 'integer', 'min:0'], 'sisa_cuti' => ['nullable', 'integer', 'min:0'],
'id_role' => ['required', 'exists:roles,id_role'], '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 App\Enums\StatusVerifikasiWajah;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; 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\DB;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
@ -13,7 +15,7 @@
class RetrainAllModels implements ShouldQueue class RetrainAllModels implements ShouldQueue
{ {
use Queueable; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $timeout = 3600; public $timeout = 3600;

View File

@ -65,7 +65,7 @@ protected static function booted()
'tanggal_selesai', 'tanggal_selesai',
'alasan', 'alasan',
'bukti_file', 'bukti_file',
'alasan_penolakan',
]; ];
public function user() 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'); 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 [ return [
'verified' => ($output['match'] ?? false) === true, 'verified' => ($output['match'] ?? false) === true,
'confidence' => $output['confidence'] ?? null, 'confidence' => $output['confidence'] ?? null,
'svm_confidence' => $output['svm_confidence'] ?? null, 'svm_confidence' => $output['svm_confidence'] ?? null,
'normalized_distance' => $output['normalized_distance'] ?? null,
'verification_status' => $output['verification_status'] ?? 'UNKNOWN', 'verification_status' => $output['verification_status'] ?? 'UNKNOWN',
'blur_score' => $output['blur_score'] ?? null, 'blur_score' => $output['blur_score'] ?? null,
'message' => $output['message'] ?? null, 'message' => $output['message'] ?? null,
]; ];
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -199,7 +199,7 @@ def verify_face(model_dir, user_id, image_path):
"expected_user": expected_user, "expected_user": expected_user,
"unknown_confidence": round(unknown_confidence, 4), "unknown_confidence": round(unknown_confidence, 4),
"blur_score": round(blur_score, 1), "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)) print(json.dumps(result))

View File

@ -1,28 +1,29 @@
@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"> <div class="space-y-1">
@if($label) @if($label)
<label class="text-sm font-semibold text-slate-700">{{ $label }}</label> <label class="text-sm font-semibold text-slate-700">{{ $label }}</label>
@endif @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" <div class="relative">
name="{{ $name }}" <input type="date"
name="{{ $name }}"
value="{{ old($name, $value) }}" value="{{ old($name, $value) }}"
onclick="try{this.showPicker()}catch(e){}" onclick="try{this.showPicker()}catch(e){}"
onfocus="try{this.showPicker()}catch(e){}" onfocus="try{this.showPicker()}catch(e){}"
{{ $attributes->merge(['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']) }} {{ $attributes->merge(['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']) }}
> >
<div class="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none text-slate-500"> <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"> <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> <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>

View File

@ -138,10 +138,11 @@ function toggleResetBtn(value) {
function openEditModal(id, nama, sisa) { function openEditModal(id, nama, sisa) {
document.getElementById('editNama').value = nama; document.getElementById('editNama').value = nama;
document.getElementById('editSisa').value = sisa; document.getElementById('editSisa').value = sisa;
let form = document.getElementById('formEditCuti'); 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' })); window.dispatchEvent(new CustomEvent('open-modal', { detail: 'edit-cuti' }));
} }
</script> </script>

View File

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

View File

@ -1,7 +1,25 @@
@extends('layouts.app') @extends('layouts.app')
@section('title', 'Data Divisi') @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') @section('content')
<div class="space-y-6"> <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"> <input type="hidden" id="edit-id" name="id">
<div> <x-input label="Nama Divisi" name="nama_divisi" id="edit-nama" required />
<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>
<div class="flex justify-end gap-3 pt-4"> <div class="flex justify-end gap-3 pt-4">
<button type="button" x-data @click="$dispatch('close-modal', 'edit-divisi')" <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> </form>
</x-modal> </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 @endsection

View File

@ -1,3 +1,4 @@
@php use App\Enums\StatusVerifikasiWajah; @endphp
@extends('layouts.app') @extends('layouts.app')
@section('title', 'Verifikasi Wajah') @section('title', 'Verifikasi Wajah')
@ -109,12 +110,13 @@ class="!mb-0" />
@forelse ($users as $user) @forelse ($users as $user)
@php @php
$dw = $user->dataWajah; $dw = $user->dataWajah;
$faceStatus = 'unregistered'; $faceStatus = 'unregistered';
if ($dw) { if ($dw) {
if ($dw->is_verified == 0) $faceStatus = 'pending'; if ($dw->is_verified == StatusVerifikasiWajah::PENDING) $faceStatus = 'pending';
elseif ($dw->is_verified == 1) $faceStatus = 'approved'; elseif ($dw->is_verified == StatusVerifikasiWajah::APPROVED) $faceStatus = 'approved';
elseif ($dw->is_verified == 2) $faceStatus = 'rejected'; elseif ($dw->is_verified == StatusVerifikasiWajah::REJECTED) $faceStatus = 'rejected';
} }
@endphp @endphp
<tr class="hover:bg-slate-50 transition-colors"> <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('title', 'Data Hari Libur')
@section('style') @section('style')
<script src='https://cdn.jsdelivr.net/npm/fullcalendar@6.1.8/index.global.min.js'></script> <script src='https://cdn.jsdelivr.net/npm/fullcalendar@6.1.8/index.global.min.js'></script>
<style> <style>
.fc-toolbar-title { font-size: 1.15rem !important; font-weight: 700; color: #1e293b; } .fc-event {
.fc-col-header-cell { background-color: #f8fafc; padding: 8px 0; border-color: #e2e8f0; } cursor: default;
.fc-daygrid-day-number { color: #64748b; font-weight: 500; font-size: 0.85rem; } border: none !important;
.fc-day-today { background-color: #f0f9ff !important; } outline: none !important;
.fc-event { border: none; padding: 2px; } 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> </style>
@endsection @endsection
@ -22,7 +161,7 @@
<x-filter-select name="id_kantor" onchange="this.form.submit()" class="py-1.5 text-xs w-48"> <x-filter-select name="id_kantor" onchange="this.form.submit()" class="py-1.5 text-xs w-48">
<option value="">Semua Kantor (Global)</option> <option value="">Semua Kantor (Global)</option>
@foreach($kantors as $k) @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 @endforeach
</x-filter-select> </x-filter-select>
</form> </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> <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 Kalender Libur Nasional & Perusahaan
</h3> </h3>
<div id="calendar" class="min-h-[450px]"></div> <div id="holiday-calendar" class="min-h-[450px]"></div>
</div> </div>
<div class="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden"> <div class="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
@ -154,6 +293,9 @@
</form> </form>
</x-modal> </x-modal>
@endsection
@section('script')
<script> <script>
window.openEditModal = function (btn) { window.openEditModal = function (btn) {
const id = btn.dataset.id; const id = btn.dataset.id;
@ -176,7 +318,7 @@
window.holidayCalendar = window.holidayCalendar || null; window.holidayCalendar = window.holidayCalendar || null;
function initHolidayCalendar() { function initHolidayCalendar() {
var calendarEl = document.getElementById('calendar'); var calendarEl = document.getElementById('holiday-calendar');
if (!calendarEl) return; if (!calendarEl) return;
if (window.holidayCalendar) { if (window.holidayCalendar) {
@ -184,8 +326,14 @@ function initHolidayCalendar() {
window.holidayCalendar = null; 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 rawData = @json($hariLiburs->items() ?? $hariLiburs);
const calendarEvents = rawData.map(function(hl) { const calendarEvents = rawData.map(function(hl) {
return { return {
title: hl.keterangan, title: hl.keterangan,
@ -204,14 +352,25 @@ function initHolidayCalendar() {
center: 'title', center: 'title',
right: 'dayGridMonth' right: 'dayGridMonth'
}, },
buttonText: {
today: 'Hari Ini',
month: 'Bulan',
},
events: calendarEvents, events: calendarEvents,
height: 'auto', height: 'auto',
displayEventTime: false,
dayMaxEvents: 3,
moreLinkText: 'lainnya',
moreLinkClick: 'popover',
eventContent: function(arg) { eventContent: function(arg) {
return { let contentEl = document.createElement('div');
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;"> contentEl.innerHTML = `
${arg.event.title} <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">
</div>` <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; window.__holidayEventAttached = true;
document.addEventListener('turbo:load', function () { document.addEventListener('turbo:load', function () {
if (document.getElementById('calendar') && typeof initHolidayCalendar === 'function') { if (document.getElementById('holiday-calendar') && typeof initHolidayCalendar === 'function') {
initHolidayCalendar(); initHolidayCalendar();
} }
}); });
@ -236,12 +395,12 @@ function initHolidayCalendar() {
} }
if (document.readyState === 'complete' || document.readyState === 'interactive') { if (document.readyState === 'complete' || document.readyState === 'interactive') {
if (document.getElementById('calendar') && !window.holidayCalendar) { if (document.getElementById('holiday-calendar') && !window.holidayCalendar) {
setTimeout(initHolidayCalendar, 50); setTimeout(initHolidayCalendar, 50);
} }
} else { } else {
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
if (document.getElementById('calendar') && !window.holidayCalendar) { if (document.getElementById('holiday-calendar') && !window.holidayCalendar) {
setTimeout(initHolidayCalendar, 50); setTimeout(initHolidayCalendar, 50);
} }
}); });

View File

@ -31,9 +31,9 @@ class="!mb-0 w-full md:w-64" />
<x-filter-select name="status" <x-filter-select name="status"
onchange="window.location.href='{{ route('izin.index') }}?status=' + this.value + '&search={{ request('search') }}'"> onchange="window.location.href='{{ route('izin.index') }}?status=' + this.value + '&search={{ request('search') }}'">
<option value="">Semua Status</option> <option value="">Semua Status</option>
<option value="1" {{ $statusId == 1 ? 'selected' : '' }}>Pending</option> <option value="{{ \App\Enums\StatusPengajuan::PENDING }}" {{ $statusId == \App\Enums\StatusPengajuan::PENDING ? 'selected' : '' }}>Pending</option>
<option value="2" {{ $statusId == 2 ? 'selected' : '' }}>Disetujui</option> <option value="{{ \App\Enums\StatusPengajuan::DISETUJUI }}" {{ $statusId == \App\Enums\StatusPengajuan::DISETUJUI ? 'selected' : '' }}>Disetujui</option>
<option value="3" {{ $statusId == 3 ? 'selected' : '' }}>Ditolak</option> <option value="{{ \App\Enums\StatusPengajuan::DITOLAK }}" {{ $statusId == \App\Enums\StatusPengajuan::DITOLAK ? 'selected' : '' }}>Ditolak</option>
</x-filter-select> </x-filter-select>
</div> </div>
</x-page-header> </x-page-header>
@ -66,7 +66,7 @@ class="w-10 h-10 rounded-full bg-slate-100 flex items-center justify-center text
<span <span
class="px-2.5 py-1 rounded-full text-xs font-medium 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' }} {{ $item->jenisIzin->nama_izin ?? 'Unknown' }}
</span> </span>
</td> </td>
@ -81,12 +81,12 @@ class="px-2.5 py-1 rounded-full text-xs font-medium
@php @php
$statusName = $item->statusPengajuan->nama_status ?? '-'; $statusName = $item->statusPengajuan->nama_status ?? '-';
$badgeColor = match ($item->id_status) { $badgeColor = match ($item->id_status) {
2 => 'green', \App\Enums\StatusPengajuan::DISETUJUI => 'green',
3 => 'red', \App\Enums\StatusPengajuan::DITOLAK => 'red',
default => 'yellow' default => 'yellow'
}; };
@endphp @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> <x-badge color="blue">Via Surat Izin</x-badge>
@else @else
<x-badge color="{{ $badgeColor }}">{{ $statusName }}</x-badge> <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>
<td class="px-6 py-4 text-right"> <td class="px-6 py-4 text-right">
<div class="flex items-center justify-end gap-2"> <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')))" 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"> 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 Detail
</button> </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) }}" <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"> 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 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> <div>
<x-select label="Jenis Izin" name="id_jenis_izin" required> <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> <option value="{{ $j->id_jenis_izin }}">{{ $j->nama_izin }}</option>
@endforeach @endforeach
</x-select> </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" /> <x-textarea label="Alasan" name="alasan" required rows="3" />
</div> </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" /> accept=".jpeg,.png,.jpg,.pdf" />
<div class="pt-4 flex justify-end gap-3"> <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> </p>
</div> </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> <div>
<label class="block text-xs font-bold text-slate-500 uppercase tracking-wider mb-1">Bukti Dokumen</label> <label class="block text-xs font-bold text-slate-500 uppercase tracking-wider mb-1">Bukti Dokumen</label>
<div <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> @click="$dispatch('close-modal', 'detail-izin')">Tutup</x-button>
@auth @auth
<form id="form-reject-izin" action="" method="POST" class="hidden">
<form id="form-reject-izin" action="" method="POST" class="hidden">@csrf</form> @csrf
<input type="hidden" name="alasan_penolakan" id="input-alasan-penolakan">
</form>
<form id="form-approve-izin" action="" method="POST" class="hidden">@csrf</form> <form id="form-approve-izin" action="" method="POST" class="hidden">@csrf</form>
<x-button id="btn-reject" variant="danger" <x-button id="btn-reject" variant="danger" onclick="submitRejectWithAlasan()">
onclick="confirmAction(event, 'form-reject-izin', 'Status akan diubah menjadi Ditolak.', '#ef4444', 'Tolak')">
Tolak Tolak
</x-button> </x-button>
<x-button id="btn-approve" variant="primary" <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') @section('script')
<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) { function openDetailModal(data) {
document.getElementById('current-id-izin').value = data.id_izin; document.getElementById('current-id-izin').value = data.id_izin;
document.getElementById('detail-nama').innerText = data.user.nama_lengkap; document.getElementById('detail-nama').innerText = data.user.nama_lengkap;
@ -281,20 +302,16 @@ function openDetailModal(data) {
noBukti.classList.add('hidden'); noBukti.classList.add('hidden');
if (data.bukti_file) { 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'); const isImage = data.bukti_file.match(/\.(jpeg|jpg|png|gif)$/i) || data.bukti_file.includes('placehold.co');
if (isImage) { if (isImage) {
img.src = url; img.src = data.bukti_file.startsWith('http') ? data.bukti_file : fileUrl;
img.classList.remove('hidden'); img.classList.remove('hidden');
} else { } else {
link.href = url; link.href = fileUrl;
link.classList.remove('hidden'); link.classList.remove('hidden');
} }
if (data.bukti_file.startsWith('http')) {
img.src = data.bukti_file;
}
} else { } else {
noBukti.classList.remove('hidden'); noBukti.classList.remove('hidden');
} }
@ -302,13 +319,20 @@ function openDetailModal(data) {
const btnApprove = document.getElementById('btn-approve'); const btnApprove = document.getElementById('btn-approve');
const btnReject = document.getElementById('btn-reject'); const btnReject = document.getElementById('btn-reject');
const cutiInfo = document.getElementById('cuti-info-banner'); const cutiInfo = document.getElementById('cuti-info-banner');
const alasanPenolakanWrapper = document.getElementById('alasan-penolakan-wrapper');
if (btnApprove) btnApprove.classList.add('hidden'); if (btnApprove) btnApprove.classList.add('hidden');
if (btnReject) btnReject.classList.add('hidden'); if (btnReject) btnReject.classList.add('hidden');
if (cutiInfo) cutiInfo.classList.add('hidden'); if (cutiInfo) cutiInfo.classList.add('hidden');
if (alasanPenolakanWrapper) alasanPenolakanWrapper.classList.add('hidden');
if (data.id_status == 1) { if (data.id_status == STATUS_DITOLAK && data.alasan_penolakan) {
if (data.id_jenis_izin == 2) { 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'); if (cutiInfo) cutiInfo.classList.remove('hidden');
} else { } else {
if (btnApprove) btnApprove.classList.remove('hidden'); if (btnApprove) btnApprove.classList.remove('hidden');
@ -319,6 +343,15 @@ function openDetailModal(data) {
window.dispatchEvent(new CustomEvent('open-modal', { detail: 'detail-izin' })); 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() { function initCreateIzinForm() {
const createForm = document.querySelector('form[action="{{ route('izin.store') }}"]'); const createForm = document.querySelector('form[action="{{ route('izin.store') }}"]');
if (createForm) { if (createForm) {

View File

@ -1,7 +1,25 @@
@extends('layouts.app') @extends('layouts.app')
@section('title', 'Data Jabatan') @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') @section('content')
<div class="space-y-6"> <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"> <input type="hidden" id="edit-id" name="id">
<div> <x-input label="Nama Jabatan" name="nama_jabatan" id="edit-nama" required />
<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>
<div class="flex justify-end gap-3 pt-4"> <div class="flex justify-end gap-3 pt-4">
<button type="button" x-data @click="$dispatch('close-modal', 'edit-jabatan')" <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> </form>
</x-modal> </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 @endsection

View File

@ -13,75 +13,23 @@
<form action="{{ route('jadwal.store') }}" method="POST"> <form action="{{ route('jadwal.store') }}" method="POST">
@csrf @csrf
<style> <div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
input[type="date"]::-webkit-calendar-picker-indicator, <x-date-input label="Tanggal Mulai" name="tanggal_mulai"
input[type="date"]::-webkit-inner-spin-button { value="{{ old('tanggal_mulai', now()->format('Y-m-d')) }}" required />
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="space-y-1"> <x-date-input label="Tanggal Selesai" name="tanggal_selesai"
<label class="text-sm font-semibold text-slate-700">Tanggal Selesai</label> value="{{ old('tanggal_selesai', now()->addDays(6)->format('Y-m-d')) }}" required />
<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>
<div class="space-y-1"> <x-select label="Shift" name="id_shift" id="id_shift" class="!mb-0" required>
<label class="text-sm font-semibold text-slate-700">Shift</label> <option value="">-- Pilih Shift --</option>
<div class="relative"> @foreach($shifts as $shift)
<select name="id_shift" id="id_shift" required <option value="{{ $shift->id_shift }}" {{ old('id_shift') == $shift->id_shift ? 'selected' : '' }}>
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"> {{ $shift->nama_shift }}
<option value="">-- Pilih Shift --</option> ({{ \Carbon\Carbon::parse($shift->jam_mulai)->format('H:i') }} -
@foreach($shifts as $shift) {{ \Carbon\Carbon::parse($shift->jam_selesai)->format('H:i') }})
<option value="{{ $shift->id_shift }}">{{ $shift->nama_shift }} </option>
({{ \Carbon\Carbon::parse($shift->jam_mulai)->format('H:i') }} - @endforeach
{{ \Carbon\Carbon::parse($shift->jam_selesai)->format('H:i') }}) </x-select>
</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>
</div> </div>
<div class="mb-6"> <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>
<x-modal name="bulk-delete" title="Hapus Jadwal Massal"> <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="{ x-data="{
tglMulai: '', tglMulai: '',
tglSelesai: '', 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"> <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="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> </div>
</form> </form>
</x-modal> </x-modal>
@ -469,7 +469,7 @@ function initCalendar() {
window.jadwalCalendar = new FullCalendar.Calendar(calendarEl, { window.jadwalCalendar = new FullCalendar.Calendar(calendarEl, {
initialView: 'dayGridMonth', initialView: 'dayGridMonth',
locale: 'id', locale: 'id',
initialDate: '2026-01-01', initialDate: new Date().toISOString().split('T')[0],
headerToolbar: { headerToolbar: {
left: 'prev,next today', left: 'prev,next today',
center: 'title', 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() { function confirmBulkDelete() {
const forms = document.querySelectorAll('form'); const form = document.getElementById('form-bulk-delete');
if (!form) return;
forms.forEach(form => {
const inputMulai = form.querySelector('input[name="tanggal_mulai"]'); const checkedBoxes = form.querySelectorAll('.user-checkbox:checked');
const inputSelesai = form.querySelector('input[name="tanggal_selesai"]'); 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 (checkedBoxes.length === 0) {
if (this.value) { Swal.fire('Peringatan', 'Pilih minimal 1 pegawai untuk dihapus jadwalnya.', 'warning');
inputSelesai.min = this.value; return;
if (inputSelesai.value && inputSelesai.value < this.value) { }
inputSelesai.value = this.value; if (!tglMulai || !tglSelesai) {
} Swal.fire('Peringatan', 'Tanggal mulai dan selesai wajib diisi.', 'warning');
} return;
}); }
inputSelesai.addEventListener('change', function() {
if (this.value) { const formatTgl = (d) => {
inputMulai.max = this.value; const obj = new Date(d);
if (inputMulai.value && inputMulai.value > this.value) { return obj.toLocaleDateString('id-ID', { day: 'numeric', month: 'long', year: 'numeric' });
inputMulai.value = this.value; };
}
} Swal.fire({
}); title: 'Konfirmasi Hapus Massal',
if (inputMulai.value) inputSelesai.min = inputMulai.value; 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>`,
if (inputSelesai.value) inputMulai.max = inputSelesai.value; icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#ef4444',
cancelButtonColor: '#64748b',
confirmButtonText: 'Ya, Hapus Sekarang',
cancelButtonText: 'Batal'
}).then((result) => {
if (result.isConfirmed) {
form.submit();
} }
}); });
}); }
</script> </script>
@endsection @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-page-header title="Laporan Lembur" subtitle="Rekapitulasi jam dan poin lembur pegawai bulanan.">
<x-slot:actions> <x-slot:actions>
<div class="flex gap-2"> <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> <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 Export Excel
</a> </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"> <x-select label="Bulan" name="bulan" class="!mb-0">
@for ($i = 1; $i <= 12; $i++) @for ($i = 1; $i <= 12; $i++)
<option value="{{ $i }}" {{ $bulan == $i ? 'selected' : '' }}> <option value="{{ $i }}" {{ $bulan == $i ? 'selected' : '' }}>
{{ DateTime::createFromFormat('!m', $i)->format('F') }} {{ \Carbon\Carbon::create()->month($i)->translatedFormat('F') }}
</option> </option>
@endfor @endfor
</x-select> </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> </td>
</tr> </tr>
@empty @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 @endforelse
</x-table> </x-table>
</div> </div>

View File

@ -2,6 +2,8 @@
@section('title', 'Laporan Izin & Cuti') @section('title', 'Laporan Izin & Cuti')
@php use App\Enums\StatusPengajuan; @endphp
@section('content') @section('content')
<div class="space-y-6"> <div class="space-y-6">
@ -10,6 +12,7 @@
<div class="flex gap-2"> <div class="flex gap-2">
<x-button type="link" <x-button type="link"
href="{{ route('laporan.izin.export', ['bulan' => $bulan, 'tahun' => $tahun, 'id_divisi' => $divisiId, 'search' => $search]) }}" 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]"> 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> <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 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) @if($izin->tanggal_mulai == $izin->tanggal_selesai)
{{ \Carbon\Carbon::parse($izin->tanggal_mulai)->translatedFormat('d M Y') }} {{ \Carbon\Carbon::parse($izin->tanggal_mulai)->translatedFormat('d M Y') }}
@else @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 @endif
</td> </td>
<td class="px-6 py-4 text-center whitespace-nowrap"> <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"> <td class="px-6 py-4 text-center whitespace-nowrap">
@php @php
$color = 'yellow'; $label = 'Menunggu'; $color = 'yellow'; $label = 'Menunggu';
if($izin->id_status == 2) { $color = 'green'; $label = 'Disetujui'; } if($izin->id_status == StatusPengajuan::DISETUJUI) { $color = 'green'; $label = 'Disetujui'; }
elseif($izin->id_status == 3) { $color = 'red'; $label = 'Ditolak'; } elseif($izin->id_status == StatusPengajuan::DITOLAK) { $color = 'red'; $label = 'Ditolak'; }
@endphp @endphp
<x-badge color="{{ $color }}"> <x-badge color="{{ $color }}">
{{ $label }} {{ $label }}

View File

@ -1,4 +1,4 @@
@extends('layouts.app') @extends('layouts.app')
@section('title', 'Input Lembur Manual') @section('title', 'Input Lembur Manual')
@ -32,7 +32,7 @@
<option value="">-- Pilih Kompensasi --</option> <option value="">-- Pilih Kompensasi --</option>
@foreach($kompensasi as $k) @foreach($kompensasi as $k)
<option value="{{ $k->id_kompensasi }}" {{ old('id_kompensasi') == $k->id_kompensasi ? 'selected' : '' }}> <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> </option>
@endforeach @endforeach
</x-select> </x-select>

View File

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

View File

@ -33,7 +33,7 @@ class="grid grid-cols-1 lg:grid-cols-3 gap-6">
required /> required />
</div> </div>
<div class="w-full"> <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>
<div class="w-full"> <div class="w-full">
<x-input label="Sisa Cuti (Hari)" name="sisa_cuti" type="number" value="12" <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> <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="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="w-full"> <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> <option value="">Pilih Divisi</option>
@foreach ($divisi as $d) @foreach ($divisi as $d)
<option value="{{ $d->id_divisi }}">{{ $d->nama_divisi }}</option> <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>
<div class="w-full"> <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> <option value="">Pilih Jabatan</option>
@foreach ($jabatan as $j) @foreach ($jabatan as $j)
<option value="{{ $j->id_jabatan }}">{{ $j->nama_jabatan }}</option> <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>
<div class="w-full"> <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> <option value="">Pilih Kantor</option>
@foreach ($kantor as $k) @foreach ($kantor as $k)
<option value="{{ $k->id_kantor }}">{{ $k->nama_kantor }}</option> <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>
<div class="w-full"> <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> <option value="">Pilih Role Akses</option>
@foreach ($roles as $r) @foreach ($roles as $r)
<option value="{{ $r->id_role }}">{{ $r->nama_role }}</option> <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> </x-select>
</div> </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 /> <x-date-input label="Tanggal Bergabung" name="tgl_bergabung" required />
</div> </div>
</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 /> <x-input label="Email" name="email" type="email" value="{{ $pegawai->email }}" required />
</div> </div>
<div class="w-full"> <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>
<div class="w-full"> <div class="w-full">
<x-input label="Sisa Cuti (Hari)" name="sisa_cuti" type="number" value="{{ $pegawai->sisa_cuti }}" <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"> <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> <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"> <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) @foreach ($divisi as $d)
<option value="{{ $d->id_divisi }}" <option value="{{ $d->id_divisi }}"
{{ $pegawai->id_divisi == $d->id_divisi ? 'selected' : '' }}> {{ $pegawai->id_divisi == $d->id_divisi ? 'selected' : '' }}>
@ -57,7 +57,7 @@ class="grid grid-cols-1 lg:grid-cols-3 gap-6">
@endforeach @endforeach
</x-select> </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) @foreach ($jabatan as $j)
<option value="{{ $j->id_jabatan }}" <option value="{{ $j->id_jabatan }}"
{{ $pegawai->id_jabatan == $j->id_jabatan ? 'selected' : '' }}>{{ $j->nama_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 @endforeach
</x-select> </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) @foreach ($kantor as $k)
<option value="{{ $k->id_kantor }}" <option value="{{ $k->id_kantor }}"
{{ $pegawai->id_kantor == $k->id_kantor ? 'selected' : '' }}> {{ $pegawai->id_kantor == $k->id_kantor ? 'selected' : '' }}>
@ -73,12 +73,12 @@ class="grid grid-cols-1 lg:grid-cols-3 gap-6">
@endforeach @endforeach
</x-select> </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="1" {{ $pegawai->status_aktif == 1 ? 'selected' : '' }}>Aktif</option>
<option value="0" {{ $pegawai->status_aktif == 0 ? 'selected' : '' }}>Non-Aktif</option> <option value="0" {{ $pegawai->status_aktif == 0 ? 'selected' : '' }}>Non-Aktif</option>
</x-select> </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) @foreach ($roles as $r)
<option value="{{ $r->id_role }}" <option value="{{ $r->id_role }}"
{{ $pegawai->roles->contains('id_role', $r->id_role) ? 'selected' : '' }}> {{ $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') }} {{ \Carbon\Carbon::parse($presensi->tanggal)->translatedFormat('d M Y') }}
</td> </td>
<td class="px-6 py-4 text-sm text-slate-600 text-center whitespace-nowrap"> <td class="px-6 py-4 text-sm text-slate-600 text-center whitespace-nowrap">
{{ $presensi->jadwal->shift->nama_shift ?? 'Shift Default' }}<br> {{ $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> <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>
<td class="px-6 py-4 text-center whitespace-nowrap"> <td class="px-6 py-4 text-center whitespace-nowrap">
<div class="flex flex-col text-sm items-center justify-center"> <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') : '--:--' }} {{ $presensi->jam_masuk ? \Carbon\Carbon::parse($presensi->jam_masuk)->format('H:i') : '--:--' }}
</span> </span>
<span class="text-xs text-slate-400 font-mono mt-0.5">s/d</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') @section('title', 'Penggunaan Poin')
@ -20,33 +20,33 @@
</x-slot:header> </x-slot:header>
@forelse ($penggunaan as $index => $item) @forelse ($penggunaan as $index => $item)
<tr class="hover:bg-gray-50 transition-colors"> <tr class="hover:bg-slate-50 transition-colors">
<td class="px-6 py-4 text-sm text-gray-700"> <td class="px-6 py-4 text-sm text-slate-700">
{{ $penggunaan->firstItem() + $index }} {{ $penggunaan->firstItem() + $index }}
</td> </td>
<td class="px-6 py-4"> <td class="px-6 py-4">
<div class="text-sm font-medium text-gray-900">{{ $item->user->nama_lengkap ?? '-' }}</div> <div class="text-sm font-medium text-slate-900">{{ $item->user->nama_lengkap ?? '-' }}</div>
<div class="text-xs text-gray-500">{{ $item->user->divisi->nama_divisi ?? '' }}</div> <div class="text-xs text-slate-500">{{ $item->user->divisi->nama_divisi ?? '' }}</div>
</td> </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') : '-' }} {{ $item->tanggal_penggunaan ? $item->tanggal_penggunaan->translatedFormat('d F Y') : '-' }}
</td> </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' }} {{ $item->jenisPengurangan->nama_pengurangan ?? 'Lainnya' }}
</td> </td>
<td class="px-6 py-4"> <td class="px-6 py-4">
<span class="font-bold text-amber-600">{{ $item->jumlah_poin }} Poin</span> <span class="font-bold text-amber-600">{{ $item->jumlah_poin }} Poin</span>
</td> </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') : '-' }} {{ $item->tanggal_diajukan ? $item->tanggal_diajukan->translatedFormat('d M Y H:i') : '-' }}
</td> </td>
<td class="px-6 py-4"> <td class="px-6 py-4">
@php @php
$idStatus = $item->id_status; $idStatus = $item->id_status;
$badgeColor = match ($idStatus) { $badgeColor = match ($idStatus) {
1 => 'yellow', \App\Enums\StatusPengajuan::PENDING => 'yellow',
2 => 'green', \App\Enums\StatusPengajuan::DISETUJUI => 'green',
3 => 'red', \App\Enums\StatusPengajuan::DITOLAK => 'red',
default => 'gray' default => 'gray'
}; };
@endphp @endphp
@ -55,9 +55,9 @@
</x-badge> </x-badge>
</td> </td>
<td class="px-6 py-4 text-right"> <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"> <div class="flex justify-end gap-2">
<form action="{{ route('penggunaan-poin.update', $item->id_penggunaan) }}" method="POST"> <form action="{{ route('penggunaan-poin.update', $item->id_penggunaan) }}" method="POST">
@csrf @csrf
@method('PUT') @method('PUT')
@ -65,10 +65,7 @@
<button type="submit" <button type="submit"
class="text-green-600 hover:text-green-900 bg-green-50 hover:bg-green-100 p-2 rounded-lg transition-all" class="text-green-600 hover:text-green-900 bg-green-50 hover:bg-green-100 p-2 rounded-lg transition-all"
title="Setujui"> title="Setujui">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <span class="text-xs font-semibold px-1">Setuju</span>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M5 13l4 4L19 7"></path>
</svg>
</button> </button>
</form> </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) }}')" 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" class="text-red-600 hover:text-red-900 bg-red-50 hover:bg-red-100 p-2 rounded-lg transition-all"
title="Tolak"> title="Tolak">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <span class="text-xs font-semibold px-1">Tolak</span>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button> </button>
</div> </div>
@else @else
<span class="text-xs text-gray-400 italic">Selesai</span> <span class="text-xs text-slate-400 italic">Selesai</span>
@endif @endif
</td> </td>
</tr> </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" /> placeholder="Jelaskan alasan kenapa pengajuan ini ditolak..." required rows="3" />
</div> </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 <x-button type="button" variant="secondary" x-data
@click="$dispatch('close-modal', 'reject-modal')">Batal</x-button> @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> <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') @extends('layouts.app')
@section('title', 'Buat Pengumuman')
@section('content') @section('content')
<div class="max-w-2xl mx-auto"> <div class="max-w-2xl mx-auto">
<x-page-header title="Buat Pengumuman Baru" subtitle="Isi form berikut untuk membuat pengumuman" /> <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"> <form action="{{ route('pengumuman.store') }}" method="POST" enctype="multipart/form-data">
@csrf @csrf
<div class="space-y-5"> <div class="space-y-5">

View File

@ -1,11 +1,13 @@
@extends('layouts.app') @extends('layouts.app')
@section('title', 'Edit Pengumuman')
@section('content') @section('content')
<div class="max-w-2xl mx-auto"> <div class="max-w-2xl mx-auto">
<x-page-header title="Edit Pengumuman" subtitle="Perbarui informasi pengumuman" /> <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"> <form action="{{ route('pengumuman.update', $pengumuman->id_pengumuman) }}" method="POST" enctype="multipart/form-data">
@csrf @csrf
@method('PUT') @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> <th class="px-6 py-5 text-xs font-bold text-slate-500 uppercase tracking-wider text-right">Aksi</th>
</x-slot:header> </x-slot:header>
@forelse ($pengumuman as $item) @forelse ($pengumuman as $item)
<tr class="hover:bg-gray-50 transition-colors"> <tr class="hover:bg-slate-50 transition-colors">
<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">
{{ $loop->iteration + ($pengumuman->currentPage() - 1) * $pengumuman->perPage() }} {{ $loop->iteration + ($pengumuman->currentPage() - 1) * $pengumuman->perPage() }}
</td> </td>
<td class="px-6 py-4"> <td class="px-6 py-4">
<div class="text-sm font-medium text-gray-900">{{ $item->judul }}</div> <div class="text-sm font-medium text-slate-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 text-slate-500 truncate max-w-xs">{{ Str::limit($item->isi, 50) }}</div>
@if($item->foto || $item->lampiran) @if($item->foto || $item->lampiran)
<div class="flex gap-2 mt-1"> <div class="flex gap-2 mt-1">
@if($item->foto) @if($item->foto)
@ -47,10 +47,10 @@
</div> </div>
@endif @endif
</td> </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->tanggal->format('d M Y') }} {{ $item->tanggal ? $item->tanggal->translatedFormat('d M Y') : '-' }}
</td> </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' }} {{ $item->pembuat->nama_lengkap ?? 'System' }}
</td> </td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"> <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">

View File

@ -1,5 +1,7 @@
@extends('layouts.app') @extends('layouts.app')
@php use App\Enums\StatusValidasi; @endphp
@section('title', 'Monitoring Presensi') @section('title', 'Monitoring Presensi')
@section('style') @section('style')
@ -24,7 +26,7 @@
<x-page-header title="Monitoring Presensi" subtitle="Pantau kehadiran pegawai harian."> <x-page-header title="Monitoring Presensi" subtitle="Pantau kehadiran pegawai harian.">
<div class="flex gap-2"> <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]"> <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"> <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> <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> </x-slot:header>
@forelse($presensi as $p) @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"> <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" /> <x-checkbox name="presensi_ids[]" value="{{ $p->id_presensi }}" class="presensi-checkbox" />
@endif @endif
</td> </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>
<td class="px-6 py-4 text-right"> <td class="px-6 py-4 text-right">
<div class="flex items-center justify-end gap-2"> <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"> <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> <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> </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> </button>
@endif @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"> <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> <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> </a>

View File

@ -2,6 +2,40 @@
@section('title', 'Pengaturan Akun') @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') @section('content')
<div class="max-w-3xl mx-auto space-y-6"> <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>
<div class="text-center"> <div class="text-center">
<h3 class="font-bold text-slate-800">{{ auth()->user()->nama_lengkap }}</h3> <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>
</div> </div>
@ -74,7 +108,7 @@ class="absolute bottom-0 right-0 p-2 bg-white rounded-full shadow-lg border bord
</div> </div>
<div class="flex justify-end pt-6 border-t border-slate-100"> <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"> 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 Simpan Perubahan
</button> </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> </div>
</div> </div>
@endsection
{{-- Cropper.js CDN --}} @section('script')
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.6.2/cropper.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.6.2/cropper.min.js"></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> <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 @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) @forelse($roles as $index => $role)
@php @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 @endphp
<tr class="hover:bg-slate-50 transition border-b border-slate-50 last:border-b-0"> <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> <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') @section('title', 'Detail Surat Izin')
@ -191,29 +191,25 @@
</div> </div>
@endif @endif
</div> </div>
@endsection
@section('style')
<style> @section('style')
@media print { <style>
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; } @media print {
/* Sembunyikan elemen layout umum (sidebar & header) */ body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
aside, header { aside, header {
display: none !important; display: none !important;
} }
body, html, #main-scroll-container, main, .h-screen, .overflow-hidden, .overflow-y-auto {
/* Reset constraint agar browser dapat memproses multi-halaman jika suratnya panjang */ height: auto !important;
body, html, #main-scroll-container, main, .h-screen, .overflow-hidden, .overflow-y-auto { min-height: auto !important;
height: auto !important; overflow: visible !important;
min-height: auto !important; background-color: transparent !important;
overflow: visible !important; margin: 0 !important;
background-color: transparent !important; padding: 0 !important;
margin: 0 !important; position: static !important;
padding: 0 !important; }
position: static !important; @page { margin: 1.5cm; }
} }
</style>
@page { margin: 1.5cm; }
}
</style>
@endsection
@endsection @endsection

View File

@ -1,4 +1,4 @@
@extends('layouts.app') @extends('layouts.app')
@section('title', 'Tukar Shift Kerja') @section('title', 'Tukar Shift Kerja')
@ -190,7 +190,7 @@ function initTukarShiftForm() {
form.addEventListener('submit', function (e) { form.addEventListener('submit', function (e) {
if (user1Select.value === user2Select.value && user1Select.value !== '') { if (user1Select.value === user2Select.value && user1Select.value !== '') {
e.preventDefault(); 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) { } catch (error) {
console.error('Error fetching schedules:', error); console.error('Error fetching schedules:', error);
targetSelect.innerHTML = '<option value="">Gagal mengambil data</option>'; 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 { } finally {
if (loadingEl) { loadingEl.classList.add('hidden'); } if (loadingEl) { loadingEl.classList.add('hidden'); }
if (arrowEl) { arrowEl.classList.remove('hidden'); } if (arrowEl) { arrowEl.classList.remove('hidden'); }
@ -319,5 +319,6 @@ function filterAllPegawaiByKantor() {
document.addEventListener('DOMContentLoaded', initTukarShiftForm); document.addEventListener('DOMContentLoaded', initTukarShiftForm);
} }
</script> </script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script src="{{ asset('js/form-handler.js') }}"></script> <script src="{{ asset('js/form-handler.js') }}"></script>
@endsection @endsection