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,35 +57,15 @@ 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>
<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">
<form id="formEditCuti" method="POST" action="">
@csrf @csrf
@method('PUT') @method('PUT')
<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> <div>
<label class="block text-sm font-semibold text-slate-700 mb-1">Pegawai</label> <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"> <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">
@ -107,49 +79,20 @@ class="block w-full pl-10 pr-3 py-2 border border-slate-300 rounded-lg focus:rin
<span class="font-bold block mb-1">Catatan:</span> <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. Perubahan ini langsung memengaruhi jatah cuti pegawai yang dapat diajukan di aplikasi mobile. Pastikan jumlahnya tepat.
</div> </div>
</div>
</div>
<div class="bg-slate-50 px-6 py-4 flex flex-row-reverse border-t border-slate-100 gap-2"> <div class="flex justify-end gap-3 pt-4 border-t border-slate-100">
<x-button type="submit" variant="primary" id="submitBtnEdit" class="w-full sm:w-auto"> <button type="button" x-data @click="$dispatch('close-modal', 'edit-cuti')"
Simpan Perubahan 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>
</x-button> <button type="submit"
<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')"> class="px-4 py-2 text-sm font-medium text-white bg-primary rounded-xl hover:bg-primary/90">Simpan Perubahan</button>
Batal
</button>
</div> </div>
</form> </form>
</div> </x-modal>
</div>
</div>
<div id="modalReset" class="fixed inset-0 z-[100] hidden overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true"> <x-modal name="reset-cuti" title="Reset Cuti Massal">
<div class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:p-0"> <form method="POST" action="{{ route('cuti.reset') }}" class="space-y-4">
<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 @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"> <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? 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> </p>
@ -174,38 +117,19 @@ class="block w-full pl-10 pr-3 py-2 border border-slate-300 rounded-lg focus:rin
oninput="toggleResetBtn(this.value)" 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"> 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>
<div class="bg-slate-50 px-6 py-4 flex flex-row-reverse border-t border-slate-100 gap-2"> <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 <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"> 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>
<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> </div>
</form> </form>
</div> </x-modal>
</div>
</div>
@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,11 +13,57 @@
<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;
}
</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">
<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> <option value="">-- Pilih Shift --</option>
@foreach($shifts as $shift) @foreach($shifts as $shift)
<option value="{{ $shift->id_shift }}">{{ $shift->nama_shift }} <option value="{{ $shift->id_shift }}">{{ $shift->nama_shift }}
@ -25,7 +71,17 @@
{{ \Carbon\Carbon::parse($shift->jam_selesai)->format('H:i') }}) {{ \Carbon\Carbon::parse($shift->jam_selesai)->format('H:i') }})
</option> </option>
@endforeach @endforeach
</x-select> </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']);