perbaikan

This commit is contained in:
jouel88 2026-03-29 11:13:50 +07:00
parent 1f881a6c69
commit 0216c572cf
26 changed files with 458 additions and 241 deletions

View File

@ -110,6 +110,10 @@ public function registerEvents(): array
$lastRow = $this->data->count() + 3; $lastRow = $this->data->count() + 3;
$dataRange = "A3:I{$lastRow}"; $dataRange = "A3:I{$lastRow}";
$sheet->getParent()->getDefaultStyle()->getFont()
->setName('Calibri')
->setSize(10);
// Merge judul // Merge judul
$sheet->mergeCells('A1:I1'); $sheet->mergeCells('A1:I1');

View File

@ -150,6 +150,10 @@ public function registerEvents(): array
$lastRow = count($this->pegawai) + 6; $lastRow = count($this->pegawai) + 6;
$dataStart = 7; $dataStart = 7;
$sheet->getParent()->getDefaultStyle()->getFont()
->setName('Calibri')
->setSize(10);
$sheet->mergeCells('A1:I1'); $sheet->mergeCells('A1:I1');
$sheet->mergeCells('A2:I2'); $sheet->mergeCells('A2:I2');
$sheet->mergeCells('A3:I3'); $sheet->mergeCells('A3:I3');

View File

@ -154,6 +154,10 @@ public function registerEvents(): array
$lastRow = count($this->rekap) + 6; $lastRow = count($this->rekap) + 6;
$dataStart = 7; $dataStart = 7;
$sheet->getParent()->getDefaultStyle()->getFont()
->setName('Calibri')
->setSize(10);
// Merge judul // Merge judul
$sheet->mergeCells('A1:K1'); $sheet->mergeCells('A1:K1');
$sheet->mergeCells('A2:K2'); $sheet->mergeCells('A2:K2');

View File

@ -22,6 +22,7 @@ public function index()
'description' => $item->isi, 'description' => $item->isi,
'tanggal' => $item->tanggal?->format('Y-m-d'), 'tanggal' => $item->tanggal?->format('Y-m-d'),
'jabatan' => $item->pembuat?->jabatan?->nama_jabatan ?? 'Admin', 'jabatan' => $item->pembuat?->jabatan?->nama_jabatan ?? 'Admin',
'nama_pembuat' => $item->pembuat?->nama_lengkap ?? 'Admin',
'avatar_url' => $item->pembuat?->foto, 'avatar_url' => $item->pembuat?->foto,
]; ];
}); });
@ -31,4 +32,23 @@ public function index()
return ApiResponse::error('Gagal memuat pengumuman: ' . $e->getMessage(), 500); return ApiResponse::error('Gagal memuat pengumuman: ' . $e->getMessage(), 500);
} }
} }
public function show($id)
{
try {
$item = Pengumuman::with('pembuat.jabatan')->findOrFail($id);
return ApiResponse::success([
'id' => $item->id_pengumuman,
'title' => $item->judul,
'description' => $item->isi,
'tanggal' => $item->tanggal?->format('Y-m-d'),
'jabatan' => $item->pembuat?->jabatan?->nama_jabatan ?? 'Admin',
'nama_pembuat' => $item->pembuat?->nama_lengkap ?? 'Admin',
'avatar_url' => $item->pembuat?->foto,
], 'Detail pengumuman berhasil dimuat');
} catch (\Exception $e) {
return ApiResponse::error('Pengumuman tidak ditemukan', 404);
}
}
} }

View File

@ -105,6 +105,61 @@ public function index()
]); ]);
} }
public function checkRadius(Request $request)
{
try {
$request->validate([
'latitude' => 'required|numeric',
'longitude' => 'required|numeric',
]);
$user = Auth::user();
$kantor = $user->kantor;
if (!$kantor) {
return response()->json([
'status' => 'error',
'message' => 'Data kantor belum disetting.',
], 400);
}
$distance = $this->calculateDistance(
$request->latitude,
$request->longitude,
$kantor->latitude,
$kantor->longitude,
);
$isWithinRadius = $distance <= $kantor->radius;
return response()->json([
'status' => 'success',
'data' => [
'is_within_radius' => $isWithinRadius,
'distance' => round($distance, 2),
'radius' => $kantor->radius,
]
]);
} catch (\Exception $e) {
return response()->json([
'status' => 'error',
'message' => $e->getMessage(),
], 500);
}
}
private function calculateDistance($lat1, $lon1, $lat2, $lon2)
{
$earthRadius = 6371000;
$dLat = deg2rad($lat2 - $lat1);
$dLon = deg2rad($lon2 - $lon1);
$a = sin($dLat / 2) * sin($dLat / 2) +
cos(deg2rad($lat1)) * cos(deg2rad($lat2)) *
sin($dLon / 2) * sin($dLon / 2);
$c = 2 * atan2(sqrt($a), sqrt(1 - $a));
return $earthRadius * $c;
}
public function store(Request $request) public function store(Request $request)
{ {
try { try {
@ -138,11 +193,16 @@ public function history(Request $request)
{ {
try { try {
$userId = Auth::id(); $userId = Auth::id();
$history = Presensi::where('id_user', $userId) $month = $request->get('month', Carbon::now('Asia/Jakarta')->month);
->latest('tanggal') $year = $request->get('year', Carbon::now('Asia/Jakarta')->year);
->paginate(10);
$enriched = $history->getCollection()->map(function ($item) use ($userId) { $history = Presensi::where('id_user', $userId)
->whereMonth('tanggal', $month)
->whereYear('tanggal', $year)
->latest('tanggal')
->get();
$enriched = $history->map(function ($item) use ($userId) {
$jadwal = \App\Models\JadwalKerja::with('shift') $jadwal = \App\Models\JadwalKerja::with('shift')
->where('id_user', $userId) ->where('id_user', $userId)
->where('tanggal', $item->tanggal) ->where('tanggal', $item->tanggal)
@ -220,9 +280,11 @@ public function history(Request $request)
]; ];
}); });
$history->setCollection($enriched); return ApiResponse::success([
'data' => $enriched->values(),
return ApiResponse::success($history); 'month' => (int) $month,
'year' => (int) $year,
]);
} catch (\Exception $e) { } catch (\Exception $e) {
return ApiResponse::error($e->getMessage(), 500); return ApiResponse::error($e->getMessage(), 500);

View File

@ -30,14 +30,23 @@ public function store(\App\Http\Requests\StorePengajuanIzinRequest $request)
$jenisIzin = JenisIzin::find($request->id_jenis_izin); $jenisIzin = JenisIzin::find($request->id_jenis_izin);
if ($jenisIzin && $jenisIzin->nama_izin == 'Cuti') { if ($jenisIzin && $jenisIzin->nama_izin == 'Cuti') {
// Validasi sisa cuti $tanggalMulai = Carbon::parse($request->tanggal_mulai);
$tanggalSelesai = Carbon::parse($request->tanggal_selesai);
$jumlahHari = $tanggalMulai->diffInDays($tanggalSelesai) + 1;
if ($user->sisa_cuti <= 0) { if ($user->sisa_cuti <= 0) {
return ApiResponse::error('Sisa cuti Anda sudah habis (0 hari). Pengajuan cuti tidak dapat dilanjutkan.', 400); return ApiResponse::error('Sisa cuti Anda sudah habis (0 hari). Pengajuan cuti tidak dapat dilanjutkan.', 400);
} }
$tanggalMulai = Carbon::parse($request->tanggal_mulai); if ($jumlahHari > 3) {
$minDate = Carbon::now()->addDays(7)->startOfDay(); return ApiResponse::error('Pengajuan cuti maksimal 3 hari berturut-turut dalam satu pengajuan.', 400);
}
if ($jumlahHari > $user->sisa_cuti) {
return ApiResponse::error("Sisa cuti Anda tidak mencukupi. Sisa: {$user->sisa_cuti} hari, diajukan: {$jumlahHari} hari.", 400);
}
$minDate = Carbon::now()->addDays(7)->startOfDay();
if ($tanggalMulai->lt($minDate)) { if ($tanggalMulai->lt($minDate)) {
return ApiResponse::error('Pengajuan Cuti minimal H-7!', 400); return ApiResponse::error('Pengajuan Cuti minimal H-7!', 400);
} }
@ -255,7 +264,7 @@ public function update(\App\Http\Requests\UpdatePengajuanIzinRequest $request, $
public function history(Request $request) public function history(Request $request)
{ {
try { try {
$query = PengajuanIzin::with(['jenisIzin', 'statusPengajuan']) $query = PengajuanIzin::with(['jenisIzin', 'statusPengajuan', 'suratIzin'])
->where('id_user', Auth::id()); ->where('id_user', Auth::id());
if ($request->has('status')) { if ($request->has('status')) {
@ -275,6 +284,16 @@ public function history(Request $request)
$history = $query->orderBy('created_at', 'desc')->paginate(10); $history = $query->orderBy('created_at', 'desc')->paginate(10);
$history->getCollection()->transform(function ($item) {
$item->bukti_file_url = $item->bukti_file
? asset('storage/' . $item->bukti_file)
: null;
$item->has_surat = $item->suratIzin !== null;
$item->id_surat = $item->suratIzin?->id_surat;
unset($item->suratIzin);
return $item;
});
return ApiResponse::success($history); return ApiResponse::success($history);
} catch (\Exception $e) { } catch (\Exception $e) {

View File

@ -200,12 +200,22 @@ public function show($id)
$historyPoin = $historyTambah->concat($historyKurang)->sortByDesc('tanggal')->take(10); $historyPoin = $historyTambah->concat($historyKurang)->sortByDesc('tanggal')->take(10);
$riwayatPresensi = \App\Models\Presensi::with(['status', 'jadwal.shift']) $riwayatPresensi = \App\Models\Presensi::with(['status'])
->where('id_user', $id) ->where('id_user', $id)
->whereDate('tanggal', '>=', now()->subDays(30)->toDateString()) ->whereDate('tanggal', '>=', now()->subDays(30)->toDateString())
->orderBy('tanggal', 'desc') ->orderBy('tanggal', 'desc')
->get(); ->get();
$jadwalKerja = \App\Models\JadwalKerja::with('shift')
->where('id_user', $id)
->whereIn('tanggal', $riwayatPresensi->pluck('tanggal'))
->get()
->keyBy('tanggal');
foreach ($riwayatPresensi as $presensi) {
$presensi->setRelation('jadwal', $jadwalKerja->get($presensi->tanggal));
}
$riwayatIzin = \App\Models\PengajuanIzin::with('jenisIzin') $riwayatIzin = \App\Models\PengajuanIzin::with('jenisIzin')
->where('id_user', $id) ->where('id_user', $id)
->whereDate('tanggal_mulai', '>=', now()->subDays(30)->toDateString()) ->whereDate('tanggal_mulai', '>=', now()->subDays(30)->toDateString())
@ -238,4 +248,18 @@ public function destroy($id)
return redirect()->route('pegawai.index')->with('success', 'Status pegawai telah diubah menjadi Non-Aktif (Resigned). Histori data tetap tersimpan.'); return redirect()->route('pegawai.index')->with('success', 'Status pegawai telah diubah menjadi Non-Aktif (Resigned). Histori data tetap tersimpan.');
} }
public function resetPassword($id)
{
$pegawai = User::findOrFail($id);
$defaultPassword = 'Mpg123!';
$pegawai->update([
'password' => Hash::make($defaultPassword),
]);
$pegawai->tokens()->delete();
return redirect()->route('pegawai.show', $id)
->with('success', "Password {$pegawai->nama_lengkap} berhasil direset ke default (Mpg123!).");
}
} }

View File

@ -65,7 +65,18 @@ public function store(Request $request)
$jenisIzin = JenisIzin::find($request->id_jenis_izin); $jenisIzin = JenisIzin::find($request->id_jenis_izin);
if ($jenisIzin && $jenisIzin->nama_izin == 'Cuti') { if ($jenisIzin && $jenisIzin->nama_izin == 'Cuti') {
$diffParams = Carbon::parse($request->tanggal_mulai, 'Asia/Jakarta')->diffInDays(Carbon::now('Asia/Jakarta')); $pegawai = User::find($request->id_user);
$tanggalMulai = Carbon::parse($request->tanggal_mulai);
$tanggalSelesai = Carbon::parse($request->tanggal_selesai);
$jumlahHari = $tanggalMulai->diffInDays($tanggalSelesai) + 1;
if ($jumlahHari > 3) {
return back()->withInput()->with('error', 'Pengajuan cuti maksimal 3 hari berturut-turut dalam satu pengajuan.');
}
if ($pegawai && $pegawai->sisa_cuti < $jumlahHari) {
return back()->withInput()->with('error', "Sisa cuti pegawai tidak mencukupi. Sisa: {$pegawai->sisa_cuti} hari, diajukan: {$jumlahHari} hari.");
}
if (Carbon::parse($request->tanggal_mulai, 'Asia/Jakarta')->diffInDays(Carbon::now('Asia/Jakarta')) < 7) { if (Carbon::parse($request->tanggal_mulai, 'Asia/Jakarta')->diffInDays(Carbon::now('Asia/Jakarta')) < 7) {
return back()->withInput()->with('error', 'Pengajuan Cuti minimal H-7!'); return back()->withInput()->with('error', 'Pengajuan Cuti minimal H-7!');
@ -207,7 +218,8 @@ public function approve($id)
if ($izin->id_jenis_izin == 2) { if ($izin->id_jenis_izin == 2) {
$user = \App\Models\User::find($izin->id_user); $user = \App\Models\User::find($izin->id_user);
$jumlahHari = Carbon::parse($izin->tanggal_mulai)->diffInDays(Carbon::parse($izin->tanggal_selesai)) + 1; $jumlahHari = Carbon::parse($izin->tanggal_mulai)->diffInDays(Carbon::parse($izin->tanggal_selesai)) + 1;
$user->decrement('sisa_cuti', $jumlahHari); $newSisa = max(0, ($user->sisa_cuti ?? 0) - $jumlahHari);
$user->update(['sisa_cuti' => $newSisa]);
} }
DB::commit(); DB::commit();

View File

@ -64,10 +64,13 @@ public function show($id)
$canApprove = false; $canApprove = false;
$tahapApproval = null; $tahapApproval = null;
if ($surat->status_surat === 'menunggu_manajer' && ($user->roles->contains('nama_role', 'manajer') || $isGlobalAdmin)) { // Cegah user yang sama menandatangani di dua tahap berbeda
$sudahApprove = $surat->approvals->contains('id_approver', $user->id);
if (!$sudahApprove && $surat->status_surat === 'menunggu_manajer' && ($user->roles->contains('nama_role', 'manajer') || $isGlobalAdmin)) {
$canApprove = true; $canApprove = true;
$tahapApproval = 1; $tahapApproval = 1;
} elseif ($surat->status_surat === 'menunggu_hrd' && ($user->roles->contains('nama_role', 'hrd') || $isGlobalAdmin)) { } elseif (!$sudahApprove && $surat->status_surat === 'menunggu_hrd' && ($user->roles->contains('nama_role', 'hrd') || $isGlobalAdmin)) {
$canApprove = true; $canApprove = true;
$tahapApproval = 2; $tahapApproval = 2;
} }
@ -95,6 +98,15 @@ public function approve(Request $request, $id)
$tahap = null; $tahap = null;
// Cegah approver yang sama menandatangani di dua posisi berbeda
$sudahApprove = ApprovalSurat::where('id_surat', $surat->id_surat)
->where('id_approver', $user->id)
->exists();
if ($sudahApprove) {
return redirect()->back()->with('error', 'Anda sudah menyetujui surat ini sebelumnya dan tidak dapat menandatangani di posisi lain.');
}
if ($surat->status_surat === 'menunggu_manajer' && ($user->roles->contains('nama_role', 'manajer') || $isGlobalAdmin)) { if ($surat->status_surat === 'menunggu_manajer' && ($user->roles->contains('nama_role', 'manajer') || $isGlobalAdmin)) {
$tahap = 1; $tahap = 1;
} elseif ($surat->status_surat === 'menunggu_hrd' && ($user->roles->contains('nama_role', 'hrd') || $isGlobalAdmin)) { } elseif ($surat->status_surat === 'menunggu_hrd' && ($user->roles->contains('nama_role', 'hrd') || $isGlobalAdmin)) {
@ -176,6 +188,15 @@ public function reject(Request $request, $id)
$tahap = null; $tahap = null;
// Cegah approver yang sama menandatangani di dua posisi berbeda
$sudahApprove = ApprovalSurat::where('id_surat', $surat->id_surat)
->where('id_approver', $user->id)
->exists();
if ($sudahApprove) {
return redirect()->back()->with('error', 'Anda sudah menyetujui surat ini sebelumnya dan tidak dapat menolak di posisi lain.');
}
if ($surat->status_surat === 'menunggu_manajer' && ($user->roles->contains('nama_role', 'manajer') || $isGlobalAdmin)) { if ($surat->status_surat === 'menunggu_manajer' && ($user->roles->contains('nama_role', 'manajer') || $isGlobalAdmin)) {
$tahap = 1; $tahap = 1;
} elseif ($surat->status_surat === 'menunggu_hrd' && ($user->roles->contains('nama_role', 'hrd') || $isGlobalAdmin)) { } elseif ($surat->status_surat === 'menunggu_hrd' && ($user->roles->contains('nama_role', 'hrd') || $isGlobalAdmin)) {
@ -258,7 +279,8 @@ private function prosesPersetujuanFinal(SuratIzin $surat)
if ($izin->id_jenis_izin == 2) { if ($izin->id_jenis_izin == 2) {
$user = \App\Models\User::find($izin->id_user, ['*']); $user = \App\Models\User::find($izin->id_user, ['*']);
$jumlahHari = Carbon::parse($izin->tanggal_mulai)->diffInDays(Carbon::parse($izin->tanggal_selesai)) + 1; $jumlahHari = Carbon::parse($izin->tanggal_mulai)->diffInDays(Carbon::parse($izin->tanggal_selesai)) + 1;
$user->decrement('sisa_cuti', $jumlahHari); $newSisa = max(0, ($user->sisa_cuti ?? 0) - $jumlahHari);
$user->update(['sisa_cuti' => $newSisa]);
} }
} }
} }

View File

@ -94,4 +94,15 @@ public function user()
{ {
return $this->belongsTo(User::class, 'id_user'); return $this->belongsTo(User::class, 'id_user');
} }
public function status()
{
return $this->belongsTo(StatusPengajuan::class, 'id_status', 'id_status');
}
public function jadwal()
{
return $this->hasOne(JadwalKerja::class, 'id_user', 'id_user')
->whereColumn('jadwal_kerja.tanggal', 'presensi.tanggal');
}
} }

View File

@ -20,6 +20,7 @@ public function run(): void
JenisIzinSeeder::class, JenisIzinSeeder::class,
StatusPengajuanSeeder::class, StatusPengajuanSeeder::class,
JenisPenguranganSeeder::class, JenisPenguranganSeeder::class,
JenisKompensasiSeeder::class,
// 4. Data dummy (opsional, comment jika tidak diperlukan di production) // 4. Data dummy (opsional, comment jika tidak diperlukan di production)
// PresensiSeeder::class, // PresensiSeeder::class,

View File

@ -9,11 +9,16 @@ class JenisIzinSeeder extends Seeder
{ {
public function run(): void public function run(): void
{ {
$types = ['Sakit', 'Cuti', 'Izin']; $types = [
['id_jenis_izin' => 1, 'nama_izin' => 'Sakit'],
['id_jenis_izin' => 2, 'nama_izin' => 'Cuti'],
['id_jenis_izin' => 3, 'nama_izin' => 'Izin'],
];
foreach ($types as $type) { foreach ($types as $type) {
DB::table('jenis_izin')->updateOrInsert( DB::table('jenis_izin')->updateOrInsert(
['nama_izin' => $type], ['id_jenis_izin' => $type['id_jenis_izin']],
['updated_at' => now(), 'created_at' => now()] ['nama_izin' => $type['nama_izin'], 'updated_at' => now(), 'created_at' => now()]
); );
} }
} }

View File

@ -0,0 +1,27 @@
<?php
namespace Database\Seeders;
use App\Models\JenisKompensasi;
use Illuminate\Database\Seeder;
class JenisKompensasiSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
$kompensasi = [
['id_kompensasi' => 1, 'nama_kompensasi' => 'Uang Lembur'],
['id_kompensasi' => 2, 'nama_kompensasi' => 'Tambahan Poin'],
];
foreach ($kompensasi as $k) {
JenisKompensasi::updateOrCreate(
['id_kompensasi' => $k['id_kompensasi']],
['nama_kompensasi' => $k['nama_kompensasi']]
);
}
}
}

View File

@ -11,20 +11,33 @@ class PresensiStatusSeeder extends Seeder
public function run(): void public function run(): void
{ {
$statuses = [ $statuses = [
'Tepat Waktu', ['id_status' => 1, 'nama_status' => 'Tepat Waktu'],
'Terlambat', ['id_status' => 2, 'nama_status' => 'Terlambat'],
'Izin', ['id_status' => 3, 'nama_status' => 'Izin'],
'Sakit', ['id_status' => 4, 'nama_status' => 'Sakit'],
'Alpha', ['id_status' => 5, 'nama_status' => 'Alpha'],
]; ];
foreach ($statuses as $status) { foreach ($statuses as $status) {
DB::table('status_presensi')->updateOrInsert( DB::table('status_presensi')->updateOrInsert(
['nama_status' => $status], ['id_status' => $status['id_status']],
['created_at' => now(), 'updated_at' => now()] ['nama_status' => $status['nama_status'], 'created_at' => now(), 'updated_at' => now()]
); );
} }
$this->command->info('Tabel status_presensi berhasil diisi!'); $validasiStatuses = [
['id_status' => 1, 'nama_status' => 'Valid'],
['id_status' => 2, 'nama_status' => 'Pending'],
['id_status' => 3, 'nama_status' => 'Ditolak'],
];
foreach ($validasiStatuses as $status) {
DB::table('status_validasi_presensi')->updateOrInsert(
['id_status' => $status['id_status']],
['nama_status' => $status['nama_status'], 'created_at' => now(), 'updated_at' => now()]
);
}
$this->command->info('Tabel status_presensi & status_validasi_presensi berhasil diisi!');
} }
} }

View File

@ -9,11 +9,16 @@ class StatusPengajuanSeeder extends Seeder
{ {
public function run(): void public function run(): void
{ {
$statuses = ['Disetujui', 'Pending', 'Ditolak']; $statuses = [
['id_status' => 1, 'nama_status' => 'Pending'],
['id_status' => 2, 'nama_status' => 'Disetujui'],
['id_status' => 3, 'nama_status' => 'Ditolak'],
];
foreach ($statuses as $status) { foreach ($statuses as $status) {
DB::table('status_pengajuan')->updateOrInsert( DB::table('status_pengajuan')->updateOrInsert(
['nama_status' => $status], ['id_status' => $status['id_status']],
['updated_at' => now(), 'created_at' => now()] ['nama_status' => $status['nama_status'], 'updated_at' => now(), 'created_at' => now()]
); );
} }
} }

View File

@ -11,18 +11,10 @@
<div class="p-6 border-b border-slate-100 flex flex-col md:flex-row md:items-center justify-between gap-4"> <div class="p-6 border-b border-slate-100 flex flex-col md:flex-row md:items-center justify-between gap-4">
<form action="{{ route('cuti.index') }}" method="GET" class="w-full md:w-1/3"> <form action="{{ route('cuti.index') }}" method="GET" class="w-full md:w-1/3">
<div class="relative"> <x-search-input name="search" :value="$search" placeholder="Cari NIK atau Nama..." class="!mb-0 h-10" />
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-slate-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
<input type="text" name="search" value="{{ $search }}" placeholder="Cari NIK atau Nama..."
class="block w-full pl-10 pr-3 py-2 border border-slate-300 rounded-lg focus:ring-primary focus:border-primary sm:text-sm transition-colors">
</div>
</form> </form>
<button onclick="toggleModal('modalReset')" 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"> <button x-data @click="$dispatch('open-modal', 'reset-cuti')" 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">
<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="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg> </svg>
@ -31,11 +23,11 @@ class="block w-full pl-10 pr-3 py-2 border border-slate-300 rounded-lg focus:rin
</div> </div>
<x-table> <x-table>
<x-slot name="header"> <x-slot:header>
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-wider text-left">Pegawai</th> <th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-wider text-left">Pegawai</th>
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-wider text-center">Sisa Cuti</th> <th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-wider text-center">Sisa Cuti</th>
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-wider text-right">Aksi</th> <th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-wider text-right">Aksi</th>
</x-slot> </x-slot:header>
@forelse ($pegawai as $p) @forelse ($pegawai as $p)
<tr class="hover:bg-slate-50 transition-colors group"> <tr class="hover:bg-slate-50 transition-colors group">
@ -65,147 +57,79 @@ class="block w-full pl-10 pr-3 py-2 border border-slate-300 rounded-lg focus:rin
@endforelse @endforelse
</x-table> </x-table>
@if($pegawai->hasPages()) <x-pagination :paginator="$pegawai" />
<div class="border-t border-slate-100 p-4">
{{ $pegawai->links() }}
</div>
@endif
</div> </div>
</div> </div>
<div id="modalEdit" class="fixed inset-0 z-[100] hidden overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true"> <x-modal name="edit-cuti" title="Edit Sisa Cuti">
<div class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:p-0"> <form id="formEditCuti" method="POST" action="" class="space-y-4">
<div class="fixed inset-0 bg-gray-500/50 transition-opacity" aria-hidden="true" onclick="toggleModal('modalEdit')"></div> @csrf
<div class="relative bg-white rounded-2xl text-left overflow-hidden shadow-2xl transform transition-all sm:my-8 sm:max-w-md w-full border border-slate-200"> @method('PUT')
<form id="formEditCuti" method="POST" action=""> <div>
@csrf <label class="block text-sm font-semibold text-slate-700 mb-1">Pegawai</label>
@method('PUT') <input type="text" id="editNama" disabled class="w-full bg-slate-50 border border-slate-200 rounded-lg px-3 py-2 text-slate-600 font-medium">
<div class="bg-white px-6 pt-6 pb-6">
<div class="flex items-center justify-between mb-5">
<h3 class="text-xl font-bold text-slate-900" id="modal-title">Edit Sisa Cuti</h3>
<button type="button" onclick="toggleModal('modalEdit')" class="text-slate-400 hover:text-slate-500">
<span class="sr-only">Close</span>
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="space-y-4">
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1">Pegawai</label>
<input type="text" id="editNama" disabled class="w-full bg-slate-50 border border-slate-200 rounded-lg px-3 py-2 text-slate-600 font-medium">
</div>
<div>
<x-input label="Jumlah Sisa Cuti (Hari)" name="sisa_cuti" type="number" id="editSisa" required min="0" placeholder="0" />
</div>
<div class="p-3 bg-blue-50 text-blue-800 rounded-lg text-xs leading-relaxed border border-blue-100">
<span class="font-bold block mb-1">Catatan:</span>
Perubahan ini langsung memengaruhi jatah cuti pegawai yang dapat diajukan di aplikasi mobile. Pastikan jumlahnya tepat.
</div>
</div>
</div>
<div class="bg-slate-50 px-6 py-4 flex flex-row-reverse border-t border-slate-100 gap-2">
<x-button type="submit" variant="primary" id="submitBtnEdit" class="w-full sm:w-auto">
Simpan Perubahan
</x-button>
<button type="button" class="w-full sm:w-auto inline-flex justify-center rounded-lg border border-slate-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-slate-700 hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary sm:text-sm" onclick="toggleModal('modalEdit')">
Batal
</button>
</div>
</form>
</div> </div>
</div>
</div> <div>
<x-input label="Jumlah Sisa Cuti (Hari)" name="sisa_cuti" type="number" id="editSisa" required min="0" placeholder="0" />
<div id="modalReset" class="fixed inset-0 z-[100] hidden overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<div class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:p-0">
<div class="fixed inset-0 bg-gray-500/50 transition-opacity" aria-hidden="true" onclick="toggleModal('modalReset')"></div>
<div class="relative bg-white rounded-2xl text-left overflow-hidden shadow-2xl transform transition-all sm:my-8 sm:max-w-lg w-full border border-slate-200">
<form method="POST" action="{{ route('cuti.reset') }}">
@csrf
<div class="bg-white px-6 pt-6 pb-6">
<div class="flex items-center justify-between mb-5">
<div class="flex items-center">
<div class="flex-shrink-0 flex items-center justify-center h-10 w-10 rounded-full bg-slate-100 mr-3">
<svg class="h-5 w-5 text-slate-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</div>
<h3 class="text-xl font-bold text-slate-900" id="modal-title">Reset Cuti Massal</h3>
</div>
<button type="button" onclick="toggleModal('modalReset')" class="text-slate-400 hover:text-slate-500">
<span class="sr-only">Close</span>
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="space-y-4">
<p class="text-sm text-slate-600">
Fungsi ini biasanya digunakan di awal tahun untuk memberikan jatah cuti baru ke <strong>seluruh pegawai</strong>. Apakah Anda yakin ingin menetapkan ulang sisa cuti massal?
</p>
<div>
<x-input label="Jatah Cuti Baru (Hari)" name="jumlah_hari" type="number" required min="1" value="12" />
</div>
<div class="p-3 bg-amber-50 text-amber-800 rounded-lg text-xs leading-relaxed border border-amber-200 flex items-start">
<svg class="w-5 h-5 mr-2 flex-shrink-0 text-amber-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path></svg>
<div>
<span class="font-bold block mb-0.5">Peringatan:</span>
Aksi ini <strong>menimpa dan menghilangkan</strong> semua sisa cuti pegawai sebelumnya tanpa pandang bulu. Aksi ini tidak bisa dibatalkan!
</div>
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1">
Ketik <span class="font-mono text-rose-600 bg-rose-50 px-1 rounded">RESET</span> untuk mengkonfirmasi
</label>
<input type="text" id="konfirmasiReset" placeholder="Ketik RESET di sini..."
oninput="toggleResetBtn(this.value)"
class="block w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-rose-500 focus:border-rose-500 transition-colors">
</div>
</div>
</div>
<div class="bg-slate-50 px-6 py-4 flex flex-row-reverse border-t border-slate-100 gap-2">
<button type="submit" id="btnResetSubmit" disabled
class="w-full sm:w-auto inline-flex justify-center rounded-lg border border-transparent shadow-sm px-4 py-2 bg-rose-600 text-base font-medium text-white hover:bg-rose-700 disabled:opacity-40 disabled:cursor-not-allowed focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-rose-600 sm:text-sm transition-colors">
<span id="submitBtnReset">Ya, Reset Sekarang</span>
</button>
<button type="button" class="w-full sm:w-auto inline-flex justify-center rounded-lg border border-slate-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-slate-700 hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary sm:text-sm" onclick="toggleModal('modalReset')">
Batal
</button>
</div>
</form>
</div> </div>
</div>
</div> <div class="p-3 bg-blue-50 text-blue-800 rounded-lg text-xs leading-relaxed border border-blue-100">
<span class="font-bold block mb-1">Catatan:</span>
Perubahan ini langsung memengaruhi jatah cuti pegawai yang dapat diajukan di aplikasi mobile. Pastikan jumlahnya tepat.
</div>
<div class="flex justify-end gap-3 pt-4 border-t border-slate-100">
<button type="button" x-data @click="$dispatch('close-modal', 'edit-cuti')"
class="px-4 py-2 text-sm font-medium text-slate-600 bg-white border border-slate-300 rounded-xl hover:bg-slate-50">Batal</button>
<button type="submit"
class="px-4 py-2 text-sm font-medium text-white bg-primary rounded-xl hover:bg-primary/90">Simpan Perubahan</button>
</div>
</form>
</x-modal>
<x-modal name="reset-cuti" title="Reset Cuti Massal">
<form method="POST" action="{{ route('cuti.reset') }}" class="space-y-4">
@csrf
<p class="text-sm text-slate-600">
Fungsi ini biasanya digunakan di awal tahun untuk memberikan jatah cuti baru ke <strong>seluruh pegawai</strong>. Apakah Anda yakin ingin menetapkan ulang sisa cuti massal?
</p>
<div>
<x-input label="Jatah Cuti Baru (Hari)" name="jumlah_hari" type="number" required min="1" value="12" />
</div>
<div class="p-3 bg-amber-50 text-amber-800 rounded-lg text-xs leading-relaxed border border-amber-200 flex items-start">
<svg class="w-5 h-5 mr-2 flex-shrink-0 text-amber-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path></svg>
<div>
<span class="font-bold block mb-0.5">Peringatan:</span>
Aksi ini <strong>menimpa dan menghilangkan</strong> semua sisa cuti pegawai sebelumnya tanpa pandang bulu. Aksi ini tidak bisa dibatalkan!
</div>
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1">
Ketik <span class="font-mono text-rose-600 bg-rose-50 px-1 rounded">RESET</span> untuk mengkonfirmasi
</label>
<input type="text" id="konfirmasiReset" placeholder="Ketik RESET di sini..."
oninput="toggleResetBtn(this.value)"
class="block w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-rose-500 focus:border-rose-500 transition-colors">
</div>
<div class="flex justify-end gap-3 pt-4 border-t border-slate-100">
<button type="button" x-data @click="$dispatch('close-modal', 'reset-cuti')"
class="px-4 py-2 text-sm font-medium text-slate-600 bg-white border border-slate-300 rounded-xl hover:bg-slate-50">Batal</button>
<button type="submit" id="btnResetSubmit" disabled
class="px-4 py-2 text-sm font-medium text-white bg-rose-600 rounded-xl hover:bg-rose-700 disabled:opacity-40 disabled:cursor-not-allowed transition-colors">Ya, Reset Sekarang</button>
</div>
</form>
</x-modal>
@endsection @endsection
@section('script') @section('script')
<script> <script>
function toggleModal(modalID) {
document.getElementById(modalID).classList.toggle("hidden");
// Reset field konfirmasi reset saat modal ditutup
if (modalID === 'modalReset') {
const konfirmasi = document.getElementById('konfirmasiReset');
const btn = document.getElementById('btnResetSubmit');
if (konfirmasi) konfirmasi.value = '';
if (btn) btn.disabled = true;
}
}
function toggleResetBtn(value) { function toggleResetBtn(value) {
const btn = document.getElementById('btnResetSubmit'); const btn = document.getElementById('btnResetSubmit');
btn.disabled = value !== 'RESET'; btn.disabled = value !== 'RESET';
@ -218,7 +142,7 @@ function openEditModal(id, nama, sisa) {
let form = document.getElementById('formEditCuti'); let form = document.getElementById('formEditCuti');
form.action = `/cuti/${id}`; form.action = `/cuti/${id}`;
toggleModal('modalEdit'); window.dispatchEvent(new CustomEvent('open-modal', { detail: 'edit-cuti' }));
} }
</script> </script>
@endsection @endsection

View File

@ -3,15 +3,9 @@
@section('title', 'Dashboard') @section('title', 'Dashboard')
@section('content') @section('content')
<div class="mb-8"> <x-page-header
<h1 class="text-2xl font-bold text-slate-800"> title="{{ Carbon\Carbon::now('Asia/Jakarta')->hour < 12 ? 'Selamat Pagi' : (Carbon\Carbon::now('Asia/Jakarta')->hour < 15 ? 'Selamat Siang' : 'Selamat Sore') }}, {{ Auth::user()->nama_lengkap }}! 👋"
{{ \Carbon\Carbon::now('Asia/Jakarta')->hour < 12 ? 'Selamat Pagi' : (\Carbon\Carbon::now('Asia/Jakarta')->hour < 15 ? 'Selamat Siang' : 'Selamat Sore') }}, subtitle="{{ Carbon\Carbon::now('Asia/Jakarta')->translatedFormat('l, d F Y') }}" />
{{ Auth::user()->nama_lengkap }}! 👋
</h1>
<p class="text-slate-500 mt-1 text-sm font-medium">
{{ \Carbon\Carbon::now('Asia/Jakarta')->translatedFormat('l, d F Y') }}
</p>
</div>
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4 md:gap-6 mb-8"> <div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4 md:gap-6 mb-8">

View File

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

View File

@ -1,4 +1,4 @@
@extends('layouts.app') @extends('layouts.app')
@section('title', 'Lembur') @section('title', 'Lembur')
@ -26,22 +26,22 @@
</x-slot:header> </x-slot:header>
@forelse ($lembur as $index => $item) @forelse ($lembur 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">
{{ $lembur->firstItem() + $index }} {{ $lembur->firstItem() + $index }}
</td> </td>
<td class="px-6 py-4 text-sm text-gray-700"> <td class="px-6 py-4 text-sm text-slate-700">
{{ \Carbon\Carbon::parse($item->tanggal_lembur)->translatedFormat('d F Y') }} {{ \Carbon\Carbon::parse($item->tanggal_lembur)->translatedFormat('d F Y') }}
<div class="text-xs text-gray-500"> <div class="text-xs text-slate-500">
{{ \Carbon\Carbon::parse($item->jam_mulai)->format('H:i') }} - {{ \Carbon\Carbon::parse($item->jam_mulai)->format('H:i') }} -
{{ \Carbon\Carbon::parse($item->jam_selesai)->format('H:i') }} {{ \Carbon\Carbon::parse($item->jam_selesai)->format('H:i') }}
</div> </div>
</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->jabatan->nama_jabatan ?? '' }}</div> <div class="text-xs text-slate-500">{{ $item->user->jabatan->nama_jabatan ?? '' }}</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->durasi_menit }} Menit {{ $item->durasi_menit }} Menit
</td> </td>
<td class="px-6 py-4"> <td class="px-6 py-4">
@ -51,7 +51,7 @@
<x-badge color="green">💵 Uang</x-badge> <x-badge color="green">💵 Uang</x-badge>
@endif @endif
</td> </td>
<td class="px-6 py-4 text-sm text-gray-700"> <td class="px-6 py-4 text-sm text-slate-700">
{{ Str::limit($item->keterangan, 30) }} {{ Str::limit($item->keterangan, 30) }}
</td> </td>
<td class="px-6 py-4"> <td class="px-6 py-4">
@ -91,7 +91,7 @@ class="text-red-600 hover:text-red-900 bg-red-50 hover:bg-red-100 p-2 rounded-lg
</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>
@ -114,7 +114,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

@ -2,11 +2,7 @@
@section('title', 'Notifikasi') @section('title', 'Notifikasi')
@section('content') @section('content')
<div class="mb-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4"> <x-page-header title="Notifikasi" subtitle="Daftar semua notifikasi Anda">
<div>
<h1 class="text-2xl font-bold text-slate-800">Notifikasi</h1>
<p class="text-sm text-slate-500 mt-1">Daftar semua notifikasi Anda</p>
</div>
@if($unreadCount > 0) @if($unreadCount > 0)
<form id="mark-all-read-form" onsubmit="return false;"> <form id="mark-all-read-form" onsubmit="return false;">
<x-button type="button" variant="secondary" onclick="markAllReadPage()"> <x-button type="button" variant="secondary" onclick="markAllReadPage()">
@ -15,7 +11,7 @@
</x-button> </x-button>
</form> </form>
@endif @endif
</div> </x-page-header>
<div class="bg-white rounded-2xl border border-slate-200/60 shadow-sm overflow-hidden"> <div class="bg-white rounded-2xl border border-slate-200/60 shadow-sm overflow-hidden">
@forelse($notifikasi as $item) @forelse($notifikasi as $item)
@ -62,11 +58,7 @@
@endforelse @endforelse
</div> </div>
@if($notifikasi->hasPages()) <x-pagination :paginator="$notifikasi" />
<div class="mt-6">
{{ $notifikasi->links() }}
</div>
@endif
@endsection @endsection
@section('script') @section('script')

View File

@ -1,4 +1,4 @@
@extends('layouts.app') @extends('layouts.app')
@section('title', 'Data Karyawan') @section('title', 'Data Karyawan')
@ -6,7 +6,7 @@
<div class="max-w-7xl mx-auto space-y-8"> <div class="max-w-7xl mx-auto space-y-8">
<div> <div>
<h1 class="text-2xl font-bold text-slate-800 mb-6">Data Karyawan</h1> <x-page-header title="Data Karyawan" subtitle="Kelola data pegawai perusahaan." class="mb-6" />
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
@foreach($stats['kantor_list'] as $kantor) @foreach($stats['kantor_list'] as $kantor)
@ -201,7 +201,3 @@ class="p-2 text-blue-500 hover:bg-blue-50 rounded-lg transition" title="Lihat De
</div> </div>
</div> </div>
@endsection @endsection
@section('script')
<script src="{{ asset('js/notifications.js') }}"></script>
@endsection

View File

@ -1,4 +1,4 @@
@extends('layouts.app') @extends('layouts.app')
@section('title', 'Detail Pegawai') @section('title', 'Detail Pegawai')
@ -104,6 +104,23 @@ class="w-24 h-24 rounded-full bg-slate-100 flex items-center justify-center text
</div> </div>
</div> </div>
</div> </div>
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 p-6">
<h4 class="text-sm font-bold text-slate-800 uppercase tracking-wider mb-4">Aksi Cepat</h4>
<div class="space-y-3">
<button type="button"
onclick="confirmAction(event, 'reset-password-form', 'Password akan direset ke default (Mpg123!). Pegawai harus login ulang setelah reset.', '#f59e0b', 'Ya, Reset Password')"
class="w-full flex items-center gap-3 px-4 py-3 bg-amber-50 text-amber-700 border border-amber-200 rounded-xl hover:bg-amber-100 transition text-sm font-semibold">
<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="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"></path>
</svg>
Reset Password
</button>
<form id="reset-password-form" action="{{ route('pegawai.reset-password', $pegawai->id) }}" method="POST" class="hidden">
@csrf
</form>
</div>
</div>
</div> </div>
<div class="lg:col-span-2 space-y-6"> <div class="lg:col-span-2 space-y-6">
@ -238,11 +255,11 @@ class="inline-flex py-1 px-3 rounded-full text-xs font-bold bg-red-100 text-red-
</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->jam_masuk ?? '08:00', 0, 5) }} - {{ substr($presensi->jadwal->jam_pulang ?? '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->jam_masuk ?? '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,11 +1,10 @@
@extends('layouts.app') @extends('layouts.app')
@section('title', 'Manajemen Hak Akses')
@section('content') @section('content')
<div class="space-y-6"> <div class="space-y-6">
<div> <x-page-header title="Manajemen Hak Akses" subtitle="Konfigurasi izin spesifik untuk setiap peran pengguna." />
<h1 class="text-2xl font-bold text-slate-800">Manajemen Hak Akses</h1>
<p class="text-sm text-slate-500">Konfigurasi izin spesifik untuk setiap peran pengguna.</p>
</div>
<div class="grid grid-cols-1 lg:grid-cols-4 gap-6"> <div class="grid grid-cols-1 lg:grid-cols-4 gap-6">
<div class="lg:col-span-1 space-y-6"> <div class="lg:col-span-1 space-y-6">

View File

@ -1,14 +1,10 @@
@extends('layouts.app') @extends('layouts.app')
@section('title', 'Riwayat Tukar Shift') @section('title', 'Riwayat Tukar Shift')
@section('content') @section('content')
<div class="space-y-6"> <div class="space-y-6">
<div class="flex justify-between items-center sm:hidden mb-4">
<h1 class="text-2xl font-bold text-slate-800">Riwayat Tukar Shift</h1>
</div>
<x-page-header title="Riwayat Tukar Shift" subtitle="Daftar log eksekusi penukaran jadwal kerja antar pegawai."> <x-page-header title="Riwayat Tukar Shift" subtitle="Daftar log eksekusi penukaran jadwal kerja antar pegawai.">
<div class="flex gap-2"> <div class="flex gap-2">
<x-back-button href="{{ route('jadwal.index') }}" /> <x-back-button href="{{ route('jadwal.index') }}" />
@ -24,7 +20,7 @@ class="px-5 py-2.5 bg-primary text-white text-sm font-semibold justify-center ro
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 overflow-hidden"> <div class="bg-white rounded-2xl shadow-sm border border-slate-200 overflow-hidden">
<x-table> <x-table>
<x-slot name="header"> <x-slot:header>
<tr> <tr>
<th scope="col" class="px-6 py-4 text-left font-semibold text-slate-600">Terjadi Pada</th> <th scope="col" class="px-6 py-4 text-left font-semibold text-slate-600">Terjadi Pada</th>
<th scope="col" class="px-6 py-4 text-left font-semibold text-slate-600">Pegawai 1</th> <th scope="col" class="px-6 py-4 text-left font-semibold text-slate-600">Pegawai 1</th>

View File

@ -37,6 +37,7 @@
Route::get('/presensi', [PresensiController::class, 'index']); Route::get('/presensi', [PresensiController::class, 'index']);
Route::post('/presensi', [PresensiController::class, 'store']); Route::post('/presensi', [PresensiController::class, 'store']);
Route::post('/presensi/check-radius', [PresensiController::class, 'checkRadius']);
Route::get('/presensi/history', [PresensiController::class, 'history']); Route::get('/presensi/history', [PresensiController::class, 'history']);
Route::post('/presensi/{id}/resubmit', [PresensiController::class, 'resubmit']); Route::post('/presensi/{id}/resubmit', [PresensiController::class, 'resubmit']);
@ -50,6 +51,7 @@
Route::get('/lembur/history', [LemburController::class, 'history']); Route::get('/lembur/history', [LemburController::class, 'history']);
Route::get('/pengumuman', [\App\Http\Controllers\Api\PengumumanApiController::class, 'index']); Route::get('/pengumuman', [\App\Http\Controllers\Api\PengumumanApiController::class, 'index']);
Route::get('/pengumuman/{id}', [\App\Http\Controllers\Api\PengumumanApiController::class, 'show']);
Route::get('/user', [AuthController::class, 'user']); Route::get('/user', [AuthController::class, 'user']);
@ -70,4 +72,11 @@
}); });
Route::post('/device-token', [\App\Http\Controllers\Api\NotifikasiApiController::class, 'saveDeviceToken']); Route::post('/device-token', [\App\Http\Controllers\Api\NotifikasiApiController::class, 'saveDeviceToken']);
Route::get('/kompensasi', function () {
return response()->json([
'success' => true,
'data' => \App\Models\JenisKompensasi::all(),
]);
});
}); });

View File

@ -89,6 +89,7 @@
Route::resource('shift', \App\Http\Controllers\ShiftController::class)->except(['create', 'edit', 'show']); Route::resource('shift', \App\Http\Controllers\ShiftController::class)->except(['create', 'edit', 'show']);
Route::resource('jadwal', \App\Http\Controllers\JadwalController::class)->except(['edit', 'show']); Route::resource('jadwal', \App\Http\Controllers\JadwalController::class)->except(['edit', 'show']);
Route::resource('pegawai', PegawaiController::class); Route::resource('pegawai', PegawaiController::class);
Route::post('/pegawai/{id}/reset-password', [PegawaiController::class, 'resetPassword'])->name('pegawai.reset-password');
Route::post('/hari-libur/sync', [\App\Http\Controllers\HariLiburController::class, 'syncHolidays'])->name('hari-libur.sync'); Route::post('/hari-libur/sync', [\App\Http\Controllers\HariLiburController::class, 'syncHolidays'])->name('hari-libur.sync');
Route::resource('hari-libur', \App\Http\Controllers\HariLiburController::class)->except(['create', 'edit', 'show']); Route::resource('hari-libur', \App\Http\Controllers\HariLiburController::class)->except(['create', 'edit', 'show']);