This commit is contained in:
jouel88 2026-07-01 12:06:13 +07:00
parent 0c08fefabe
commit 43429ea33c
52 changed files with 2753 additions and 518 deletions

19
.gitignore vendored
View File

@ -33,3 +33,22 @@ _ide_helper_models.php
# Research notebooks & scripts # Research notebooks & scripts
*.ipynb *.ipynb
/notebook_face_recognition.py /notebook_face_recognition.py
# Dokumen laporan TA (bukan bagian kode)
/laporan_revisi_text.txt
/proposal_text.txt
/proposal_images/
# Script utility sementara
/extract_images.py
/map_images.py
/read_docx.py
# Folder hosting upload (tidak perlu di repo)
/_hosting_upload/
# Storage zip backup
/storage/app/laravel_storage_public.zip
# Direktori aneh Windows (symlink error)
/public/%SystemDrive%/

246
DESIGN.md Normal file
View File

@ -0,0 +1,246 @@
# DESIGN.md — HRIS MP (Menjadi Pengaruh Group)
> HR Information System — Web Admin Dashboard & Employee Mobile App
## ⚠️ CRITICAL: This is a LIGHT THEME application. Do NOT use dark mode. All page backgrounds are light gray (#F8FAFC). All cards and content areas are WHITE (#FFFFFF). The sidebar is WHITE with a light gray right border. The dark navy color (#130F26) is ONLY used for: active sidebar menu items, primary action buttons, and the avatar circle. Everything else should be light, clean, and white.
---
## Brand Identity
- **Product Name**: HRIS MP
- **Company**: Menjadi Pengaruh Group
- **Design Mood**: Clean, bright, minimal, professional, corporate. Light and airy with plenty of white space.
- **Overall Feel**: Like a premium SaaS admin dashboard. Think Linear, Notion, or Vercel Dashboard — clean white interfaces with subtle shadows.
---
## Typography
- **Font**: Inter (Google Fonts), weights 300700
- **Page Title**: 24px, Bold, color #1e293b (dark slate)
- **Section Title**: 18px, Bold, color #1e293b
- **Card Label**: 14px, Semibold, UPPERCASE, color #64748b (gray)
- **Body Text**: 14px, Regular, color #1e293b
- **Secondary Text**: 14px, Regular, color #64748b (medium gray)
- **Caption/Small**: 12px, color #94a3b8 (light gray)
- **Badge Text**: 10px, Bold
---
## Color Palette
### IMPORTANT: Light Theme Colors
**Page background**: #F8FAFC (very light gray, almost white)
**Card/Content background**: #FFFFFF (pure white)
**Sidebar background**: #FFFFFF (pure white) with right border #e2e8f0
### Accent Color (used sparingly!)
- **Primary accent**: #130F26 (very dark navy) — ONLY for active menu items, primary buttons, and avatar
- **Primary gradient**: from #130F26 to #2A244A — ONLY for primary action buttons
- **Primary hover**: #1E1B3A
### Text Colors
- **Heading text**: #1e293b (dark slate) on white background
- **Body text**: #334155 (medium dark) on white background
- **Secondary text**: #64748b (medium gray) on white background
- **Placeholder text**: #94a3b8 (light gray)
- **Disabled text**: #94a3b8
### Border & Divider Colors
- **Card borders**: #f1f5f9 (very subtle, barely visible)
- **Input borders**: #e2e8f0 (light gray)
- **Table dividers**: #f1f5f9 (very light)
- **Sidebar section dividers**: #e2e8f0
### Semantic Status Colors (for badges and indicators only)
- **Success/Approved**: bg #dcfce7, text #16a34a (green)
- **Pending/Warning**: bg #fff7ed, text #f97316 (orange)
- **Rejected/Error**: bg #fef2f2, text #dc2626 (red)
- **Info**: bg #eff6ff, text #2563eb (blue)
### Action Button Colors
- **Edit button**: bg #fffbeb (light amber), text #d97706 (amber)
- **Delete button**: bg #fef2f2 (light red), text #dc2626 (red)
- **Primary button**: bg gradient #130F26→#2A244A, text white
### Stat Card Icon Backgrounds (pastel, light colors)
- Indigo: bg #eef2ff, icon #4f46e5
- Purple: bg #faf5ff, icon #9333ea
- Blue: bg #eff6ff, icon #2563eb
- Emerald: bg #ecfdf5, icon #059669
- Orange: bg #fff7ed, icon #ea580c
- Red: bg #fef2f2, icon #dc2626
---
## Shadows (subtle, not heavy)
- **Card default**: 0 2px 12px -2px rgba(0,0,0,0.04) — very subtle
- **Card hover**: 0 4px 20px -2px rgba(0,0,0,0.05) — slightly more visible
- **Button hover glow**: 0 0 15px rgba(19,15,38,0.15) — subtle navy glow
---
## Layout Structure
### Overall Page Layout
- Left: Fixed sidebar (288px wide, WHITE background, light gray right border)
- Right: Scrollable content area (light gray #F8FAFC background)
- Top of content: Sticky header bar (white with frosted glass effect)
### Sidebar (LEFT SIDE)
- Background: WHITE (#FFFFFF)
- Width: 288px
- Right border: 1px solid #e2e8f0 (light gray line)
- Top: Company logo centered (height ~112px area)
- Menu items:
- **Active item**: bg #130F26 (dark navy), text white, rounded 12px, subtle shadow
- **Inactive item**: text #64748b (gray), hover: bg #f8fafc, text #0f172a
- Icon size: 20x20px, on the left of text
- Text: 14px semibold
- Section labels: 10px, bold, uppercase, gray (#94a3b8), with horizontal lines on both sides
- Sections: "Manajemen", "Absensi", "Data Master" (collapsible with chevron)
- Bottom: "Pengaturan Akun" link + red "Logout" button
### Top Header Bar
- Background: white with 70% opacity + backdrop blur (frosted glass)
- Sticky at top
- Bottom border: 1px solid rgba(226,232,240,0.6)
- Very subtle shadow
- Left: Page title (24px, bold, #1e293b)
- Right: Notification bell icon → User name + email text → Circular avatar
- Avatar: 44px circle, gradient #130F26→#2B2545, white letter inside, white ring around it
---
## Components
### Cards
- Background: WHITE
- Border: 1px solid #f1f5f9 (barely visible)
- Border radius: 16px to 24px
- Shadow: very subtle (see shadows section)
- Padding: 24px inside
- On hover: slight lift (-1px up) with slightly stronger shadow
### Stat Cards (Dashboard)
- Background: WHITE
- Large number: 30-36px, extra bold, color #1e293b
- Label: 14px, semibold, uppercase, color #64748b
- Icon: in a pastel-colored rounded square (16px radius, 12px padding)
- Subtle decorative gradient orb in background at very low opacity (3%)
### Data Tables
- Container: WHITE background, 16px border radius, ring-1 #f1f5f9
- Header row: bg #f8fafc (almost white), text #64748b, 11px, bold, uppercase
- Body rows: WHITE background, divided by 1px #f1f5f9 lines
- Row hover: slight lift with shadow
- Cell padding: 16px vertical
### Input Fields
- Background: #f8fafc (very light gray), turns WHITE on focus
- Border: 1px solid #e2e8f0
- Border radius: 12px
- Height: 44px
- Focus state: 4px ring in rgba(19,15,38,0.1), border turns #130F26
- Label above: 14px, semibold, #334155
### Buttons
- **Primary**: gradient #130F26→#2A244A, text white, 12px radius, hover lifts up
- **Secondary**: WHITE bg, border #e2e8f0, text #334155
- **Danger**: gradient red-600→red-500, text white
- All buttons: 12px radius, padding 10px 24px
### Modals
- Overlay: #0f172a at 40% opacity + backdrop blur
- Modal box: WHITE bg, 16px radius, heavy shadow
- Header: frosted glass (white 80% + blur), title bold, close X button
- Content area: white, 24px padding
### Badges/Status Pills
- Small rounded-full pills
- Approved: light green bg #dcfce7, green text #16a34a
- Pending: light orange bg #fff7ed, orange text #f97316
- Rejected: light red bg #fef2f2, red text #dc2626
- Size: 10px bold text, padding 2px 8px
### Search Input
- Same as input field but with magnifying glass icon on the left
### Pagination
- Active page: bg #130F26, text white, rounded
- Inactive: WHITE bg, border, text #475569
### Notification Dropdown
- Bell icon (24px), with red count badge
- Dropdown: WHITE bg, 384px wide, 16px radius, heavy shadow
- Each item: icon + title + message + time
### SweetAlert Dialogs
- Rounded 20px, WHITE bg, centered
- Confirm button: #130F26 bg, full width
- Cancel button: #f1f5f9 bg, gray text
---
## Page Types
### 1. Login Page
- Split screen: Left half = full bleed image with primary color overlay, Right half = WHITE form area
- Form: centered, max 384px wide
- Title "Sign in": 30px, extra bold, #130F26
- Inputs with labels
- Full-width primary button
- Footer: copyright text
### 2. Dashboard
- Stat cards in 4-column grid (responsive)
- Charts below
- WHITE cards on light gray page background
### 3. Data Table Pages
- Page header: title + subtitle + action buttons
- Filter bar: dropdowns + search input
- WHITE table card
- Pagination
### 4. Form Pages (Create/Edit)
- Back button
- WHITE form cards with sections
- 2-column grid layout for form fields
- Save button
### 5. Modal CRUD Pages (Divisi, Jabatan, Shift, Role, etc.)
- Table page with "+ Tambah" button
- Centered modal overlay for create/edit
- Same table + modal pattern
### 6. Calendar Page (Jadwal)
- Month view calendar
- Color-coded shift events
- Legend bar
---
## Mobile App (Flutter)
- Light theme matching web
- Background: #F8FAFC
- Cards: WHITE
- Primary accent: #130F26
- Bottom navigation bar
- Card-based layouts
---
## Animations
- Button hover: lift 0.5px up + glow shadow
- Card hover: lift 1px up + stronger shadow
- Sidebar icon hover: scale to 110%
- Modal: fade + slide up + scale
- Dropdown: fade + slide down
- Table row hover: lift with shadow
- All transitions: 200-300ms ease

View File

@ -14,6 +14,7 @@ class NotifikasiApiController extends Controller
public function index(Request $request) public function index(Request $request)
{ {
$notifikasi = Notifikasi::forUser(Auth::id()) $notifikasi = Notifikasi::forUser(Auth::id())
->whereNotIn('tipe', ['training'])
->orderBy('created_at', 'desc') ->orderBy('created_at', 'desc')
->paginate(20); ->paginate(20);
@ -22,7 +23,10 @@ public function index(Request $request)
public function unreadCount() public function unreadCount()
{ {
$count = Notifikasi::forUser(Auth::id())->unread()->count(); $count = Notifikasi::forUser(Auth::id())
->whereNotIn('tipe', ['training'])
->unread()
->count();
return ApiResponse::success(['count' => $count]); return ApiResponse::success(['count' => $count]);
} }
@ -35,7 +39,10 @@ public function markAsRead($id)
public function markAllAsRead() public function markAllAsRead()
{ {
Notifikasi::forUser(Auth::id())->unread()->update(['is_read' => true]); Notifikasi::forUser(Auth::id())
->whereNotIn('tipe', ['training'])
->unread()
->update(['is_read' => true]);
return ApiResponse::success(null, 'Semua notifikasi ditandai sudah dibaca.'); return ApiResponse::success(null, 'Semua notifikasi ditandai sudah dibaca.');
} }

View File

@ -24,8 +24,8 @@ public function index()
'tanggal' => $item->tanggal?->format('Y-m-d'), 'tanggal' => $item->tanggal?->format('Y-m-d'),
'foto_url' => $item->foto ? asset('storage/' . $item->foto) : null, 'foto_url' => $item->foto ? asset('storage/' . $item->foto) : null,
'lampiran_url' => $item->lampiran ? asset('storage/' . $item->lampiran) : null, 'lampiran_url' => $item->lampiran ? asset('storage/' . $item->lampiran) : null,
'jabatan' => $item->pembuat?->jabatan?->nama_jabatan ?? 'Admin', 'jabatan' => $item->pembuat?->jabatan?->nama_jabatan ?? 'HRD',
'nama_pembuat' => $item->pembuat?->nama_lengkap ?? 'Admin', 'nama_pembuat' => $item->pembuat?->nama_lengkap ?? 'HRD',
'avatar_url' => $item->pembuat?->foto, 'avatar_url' => $item->pembuat?->foto,
]; ];
}); });
@ -48,8 +48,8 @@ public function show($id)
'tanggal' => $item->tanggal?->format('Y-m-d'), 'tanggal' => $item->tanggal?->format('Y-m-d'),
'foto_url' => $item->foto ? asset('storage/' . $item->foto) : null, 'foto_url' => $item->foto ? asset('storage/' . $item->foto) : null,
'lampiran_url' => $item->lampiran ? asset('storage/' . $item->lampiran) : null, 'lampiran_url' => $item->lampiran ? asset('storage/' . $item->lampiran) : null,
'jabatan' => $item->pembuat?->jabatan?->nama_jabatan ?? 'Admin', 'jabatan' => $item->pembuat?->jabatan?->nama_jabatan ?? 'HRD',
'nama_pembuat' => $item->pembuat?->nama_lengkap ?? 'Admin', 'nama_pembuat' => $item->pembuat?->nama_lengkap ?? 'HRD',
'avatar_url' => $item->pembuat?->foto, 'avatar_url' => $item->pembuat?->foto,
], 'Detail pengumuman berhasil dimuat'); ], 'Detail pengumuman berhasil dimuat');
} catch (\Exception $e) { } catch (\Exception $e) {

View File

@ -235,8 +235,8 @@ public function history(Request $request)
$statusMasuk = 'Sakit'; $statusMasuk = 'Sakit';
} elseif ($item->id_status != null && $item->id_status != StatusPresensi::TEPAT_WAKTU && $item->id_status != StatusPresensi::TERLAMBAT) { } elseif ($item->id_status != null && $item->id_status != StatusPresensi::TEPAT_WAKTU && $item->id_status != StatusPresensi::TERLAMBAT) {
$statusStatus = \DB::table('status_presensi')->where('id_status', $item->id_status)->value('nama_status'); $statusStatus = \App\Enums\StatusPresensi::label($item->id_status);
if ($statusStatus) { if ($statusStatus !== 'Unknown') {
$statusMasuk = $statusStatus; $statusMasuk = $statusStatus;
} }
} }

View File

@ -36,7 +36,7 @@ public function update(Request $request)
Storage::disk('public')->delete($user->foto); Storage::disk('public')->delete($user->foto);
} }
$path = $request->file('foto')->store('uploads/profil', 'public'); $path = $request->file('foto')->store('foto-profil', 'public');
$user->foto = $path; $user->foto = $path;
} }

View File

@ -10,7 +10,7 @@
use App\Jobs\ReextractAllFrames; use App\Jobs\ReextractAllFrames;
use App\Jobs\RetrainAllModels; use App\Jobs\RetrainAllModels;
use App\Jobs\MigrateExistingEmbeddings;
use App\Services\FaceRecognitionService; use App\Services\FaceRecognitionService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
@ -255,31 +255,4 @@ public function reextractAll()
return redirect()->back()->with('success', 'Job re-extract frame dari video berhasil ditambahkan ke antrian. Mohon tunggu beberapa saat untuk hasilnya.'); return redirect()->back()->with('success', 'Job re-extract frame dari video berhasil ditambahkan ke antrian. Mohon tunggu beberapa saat untuk hasilnya.');
} }
public function migrateEmbeddings()
{
dispatch(new MigrateExistingEmbeddings());
return redirect()->back()->with('success', 'Proses migrasi embedding sedang berjalan di background. Anda akan menerima notifikasi setelah selesai.');
}
public function uploadVideo(Request $request, $id)
{
$request->validate([
'video_wajah' => 'required|file|mimetypes:video/mp4,video/quicktime,video/x-msvideo,video/3gpp,video/x-matroska|max:51200',
], [
'video_wajah.required' => 'File video wajah wajib diupload.',
'video_wajah.mimetypes' => 'Format video tidak didukung. Gunakan MP4, MOV, atau AVI.',
'video_wajah.max' => 'Ukuran video terlalu besar (maksimal 50MB).',
]);
$user = User::findOrFail($id);
try {
$result = $this->faceService->enrollFace($user->id, $request->file('video_wajah'));
return redirect()->back()->with('success', "Video wajah untuk {$user->nama_lengkap} berhasil diupload. Proses ekstraksi frame sedang berjalan.");
} catch (\Exception $e) {
Log::error("Upload video wajah gagal untuk user {$id}: " . $e->getMessage());
return redirect()->back()->with('error', 'Gagal upload video: ' . $e->getMessage());
}
}
} }

View File

@ -30,7 +30,7 @@ public function index()
return $s; return $s;
}); });
$pegawaiQuery = User::where('status_aktif', 1)->bukanHrd()->with(['kantor', 'jabatan']); $pegawaiQuery = User::where('status_aktif', 1)->bukanSuperAdmin()->with(['kantor', 'jabatan']);
if (!$isGlobalAdmin) { if (!$isGlobalAdmin) {
$pegawaiQuery->where('id_kantor', $user->id_kantor); $pegawaiQuery->where('id_kantor', $user->id_kantor);
} }
@ -213,7 +213,7 @@ public function create()
$kantor = $isGlobalAdmin ? Kantor::all() : Kantor::where('id_kantor', $user->id_kantor)->get(); $kantor = $isGlobalAdmin ? Kantor::all() : Kantor::where('id_kantor', $user->id_kantor)->get();
$divisi = Divisi::all(); $divisi = Divisi::all();
$pegawaiQuery = User::where('status_aktif', 1)->bukanHrd()->with(['kantor', 'jabatan']); $pegawaiQuery = User::where('status_aktif', 1)->bukanSuperAdmin()->with(['kantor', 'jabatan']);
if (!$isGlobalAdmin) { if (!$isGlobalAdmin) {
$pegawaiQuery->where('id_kantor', $user->id_kantor); $pegawaiQuery->where('id_kantor', $user->id_kantor);
} }

View File

@ -162,7 +162,7 @@ private function buildRekap(int $bulan, int $tahun, $divisiId = null, $kantorId
$pegawaiQuery = User::with(['jabatan', 'divisi']) $pegawaiQuery = User::with(['jabatan', 'divisi'])
->where('status_aktif', 1) ->where('status_aktif', 1)
->bukanHrd() ->bukanSuperAdmin()
->select('id', 'nama_lengkap', 'nik', 'id_jabatan', 'id_divisi') ->select('id', 'nama_lengkap', 'nik', 'id_jabatan', 'id_divisi')
->orderBy('nama_lengkap', 'asc'); ->orderBy('nama_lengkap', 'asc');

View File

@ -67,7 +67,7 @@ private function buildRekap(string $bulan, string $tahun, $divisiId = null, $kan
->whereMonth('tanggal_lembur', $bulan) ->whereMonth('tanggal_lembur', $bulan)
->where('id_status', StatusPengajuan::DISETUJUI); ->where('id_status', StatusPengajuan::DISETUJUI);
}]) }])
->bukanHrd(); ->bukanSuperAdmin();
if ($kantorId) { if ($kantorId) {
$query->where('id_kantor', $kantorId); $query->where('id_kantor', $kantorId);

View File

@ -72,7 +72,7 @@ public function index(Request $request)
$usersQuery = User::with(['kantor', 'divisi', 'jabatan']) $usersQuery = User::with(['kantor', 'divisi', 'jabatan'])
->select('id', 'nama_lengkap', 'nik', 'id_kantor', 'id_divisi', 'id_jabatan') ->select('id', 'nama_lengkap', 'nik', 'id_kantor', 'id_divisi', 'id_jabatan')
->where('status_aktif', 1) ->where('status_aktif', 1)
->bukanHrd() ->bukanSuperAdmin()
->orderBy('nama_lengkap', 'asc'); ->orderBy('nama_lengkap', 'asc');
if (!$isGlobalAdmin) { if (!$isGlobalAdmin) {

View File

@ -34,6 +34,7 @@ public function store(StorePengumumanRequest $request)
{ {
$data = $request->validated(); $data = $request->validated();
$data['dibuat_oleh'] = Auth::id(); $data['dibuat_oleh'] = Auth::id();
$data['tanggal'] = now()->toDateString();
if ($request->hasFile('foto')) { if ($request->hasFile('foto')) {
$data['foto'] = $request->file('foto')->store('uploads/pengumuman/foto', 'public'); $data['foto'] = $request->file('foto')->store('uploads/pengumuman/foto', 'public');
@ -110,4 +111,31 @@ public function destroy(Pengumuman $pengumuman)
return redirect()->back()->with('error', 'Gagal menghapus pengumuman: ' . $e->getMessage()); return redirect()->back()->with('error', 'Gagal menghapus pengumuman: ' . $e->getMessage());
} }
} }
public function bulkDelete(\Illuminate\Http\Request $request)
{
try {
$ids = $request->input('ids');
if (empty($ids) || !is_array($ids)) {
return redirect()->back()->with('error', 'Tidak ada pengumuman yang dipilih.');
}
$pengumumanList = Pengumuman::whereIn('id_pengumuman', $ids)->get();
foreach ($pengumumanList as $pengumuman) {
if ($pengumuman->foto) {
Storage::disk('public')->delete($pengumuman->foto);
}
if ($pengumuman->lampiran) {
Storage::disk('public')->delete($pengumuman->lampiran);
}
$pengumuman->delete();
}
return redirect()->route('pengumuman.index')
->with('success', 'Pengumuman yang dipilih berhasil dihapus.');
} catch (\Exception $e) {
return redirect()->back()->with('error', 'Gagal menghapus pengumuman: ' . $e->getMessage());
}
}
} }

View File

@ -124,6 +124,15 @@ public function index(Request $request)
} elseif ($item->id_status == StatusPresensi::TEPAT_WAKTU) { } elseif ($item->id_status == StatusPresensi::TEPAT_WAKTU) {
$item->status_keterlambatan = 'Tepat Waktu'; $item->status_keterlambatan = 'Tepat Waktu';
$item->badge_color = 'emerald'; $item->badge_color = 'emerald';
} elseif ($item->id_status == StatusPresensi::IZIN) {
$item->status_keterlambatan = $item->alasan_telat ?? 'Izin / Cuti';
$item->badge_color = 'amber';
} elseif ($item->id_status == StatusPresensi::SAKIT) {
$item->status_keterlambatan = $item->alasan_telat ?? 'Sakit';
$item->badge_color = 'amber';
} elseif ($item->id_status == StatusPresensi::ALPHA) {
$item->status_keterlambatan = 'Alpha';
$item->badge_color = 'rose';
} }
$item->validasi_label = $item->id_validasi == StatusValidasi::VALID ? 'Disetujui' : ($item->id_validasi == StatusValidasi::DITOLAK ? 'Ditolak' : 'Pending'); $item->validasi_label = $item->id_validasi == StatusValidasi::VALID ? 'Disetujui' : ($item->id_validasi == StatusValidasi::DITOLAK ? 'Ditolak' : 'Pending');
@ -247,7 +256,7 @@ public function create()
$isGlobalAdmin = $user->isGlobalAdmin(); $isGlobalAdmin = $user->isGlobalAdmin();
$pegawaiQuery = \App\Models\User::with('divisi') $pegawaiQuery = \App\Models\User::with('divisi')
->bukanHrd() ->bukanSuperAdmin()
->orderBy('nama_lengkap', 'asc'); ->orderBy('nama_lengkap', 'asc');
if (!$isGlobalAdmin) { if (!$isGlobalAdmin) {
@ -256,7 +265,9 @@ public function create()
$pegawai = $pegawaiQuery->get(); $pegawai = $pegawaiQuery->get();
$statuses = \Illuminate\Support\Facades\DB::table('status_presensi')->get(); $statuses = collect(\App\Enums\StatusPresensi::LABELS)->map(function ($label, $id) {
return (object) ['id_status' => $id, 'nama_status' => $label];
})->values();
return view('presensi.create', compact('pegawai', 'statuses')); return view('presensi.create', compact('pegawai', 'statuses'));
} }
@ -307,7 +318,9 @@ public function edit($id)
abort(403, 'Anda tidak diizinkan mengedit presensi dari kantor lain.'); abort(403, 'Anda tidak diizinkan mengedit presensi dari kantor lain.');
} }
$statuses = \Illuminate\Support\Facades\DB::table('status_presensi')->get(); $statuses = collect(\App\Enums\StatusPresensi::LABELS)->map(function ($label, $id) {
return (object) ['id_status' => $id, 'nama_status' => $label];
})->values();
return view('presensi.edit', compact('presensi', 'statuses')); return view('presensi.edit', compact('presensi', 'statuses'));
} }
@ -322,7 +335,7 @@ public function updateManual(Request $request, $id)
} }
$request->validate([ $request->validate([
'id_status' => 'required|exists:status_presensi,id_status', 'id_status' => ['required', \Illuminate\Validation\Rule::in(array_keys(\App\Enums\StatusPresensi::LABELS))],
'jam_masuk' => [ 'jam_masuk' => [
\Illuminate\Validation\Rule::requiredIf(fn () => in_array($request->id_status, [StatusPresensi::TEPAT_WAKTU, StatusPresensi::TERLAMBAT])), \Illuminate\Validation\Rule::requiredIf(fn () => in_array($request->id_status, [StatusPresensi::TEPAT_WAKTU, StatusPresensi::TERLAMBAT])),
'nullable', 'nullable',

View File

@ -38,7 +38,7 @@ public function index()
public function create() public function create()
{ {
$pegawai = User::with('kantor')->bukanHrd()->get(); $pegawai = User::with('kantor')->bukanSuperAdmin()->get();
$kantor = \App\Models\Kantor::orderBy('nama_kantor', 'asc')->get(); $kantor = \App\Models\Kantor::orderBy('nama_kantor', 'asc')->get();

View File

@ -26,7 +26,7 @@ public function rules(): array
return [ return [
'id_user' => 'required|exists:users,id', 'id_user' => 'required|exists:users,id',
'tanggal' => 'required|date', 'tanggal' => 'required|date',
'id_status' => 'required|exists:status_presensi,id_status', 'id_status' => ['required', Rule::in(array_keys(\App\Enums\StatusPresensi::LABELS))],
'jam_masuk' => [ 'jam_masuk' => [
Rule::requiredIf(fn () => in_array($this->id_status, [1, 2])), Rule::requiredIf(fn () => in_array($this->id_status, [1, 2])),
'nullable', 'nullable',

View File

@ -16,7 +16,6 @@ public function rules(): array
return [ return [
'judul' => 'required|string|max:80', 'judul' => 'required|string|max:80',
'isi' => 'required|string|max:500', 'isi' => 'required|string|max:500',
'tanggal' => 'required|date',
'foto' => 'nullable|image|mimes:jpeg,png,jpg|max:2048', 'foto' => 'nullable|image|mimes:jpeg,png,jpg|max:2048',
'lampiran' => 'nullable|file|mimes:pdf,doc,docx,xls,xlsx|max:5120', 'lampiran' => 'nullable|file|mimes:pdf,doc,docx,xls,xlsx|max:5120',
]; ];
@ -28,7 +27,6 @@ public function messages(): array
'judul.required' => 'Judul pengumuman wajib diisi.', 'judul.required' => 'Judul pengumuman wajib diisi.',
'judul.max' => 'Judul maksimal 80 karakter.', 'judul.max' => 'Judul maksimal 80 karakter.',
'isi.required' => 'Isi pengumuman wajib diisi.', 'isi.required' => 'Isi pengumuman wajib diisi.',
'tanggal.required' => 'Tanggal tayang wajib diisi.',
]; ];
} }
} }

View File

@ -16,7 +16,6 @@ public function rules(): array
return [ return [
'judul' => 'required|string|max:80', 'judul' => 'required|string|max:80',
'isi' => 'required|string|max:500', 'isi' => 'required|string|max:500',
'tanggal' => 'required|date',
'foto' => 'nullable|image|mimes:jpeg,png,jpg|max:2048', 'foto' => 'nullable|image|mimes:jpeg,png,jpg|max:2048',
'lampiran' => 'nullable|file|mimes:pdf,doc,docx,xls,xlsx|max:5120', 'lampiran' => 'nullable|file|mimes:pdf,doc,docx,xls,xlsx|max:5120',
]; ];
@ -28,7 +27,6 @@ public function messages(): array
'judul.required' => 'Judul pengumuman wajib diisi.', 'judul.required' => 'Judul pengumuman wajib diisi.',
'judul.max' => 'Judul maksimal 80 karakter.', 'judul.max' => 'Judul maksimal 80 karakter.',
'isi.required' => 'Isi pengumuman wajib diisi.', 'isi.required' => 'Isi pengumuman wajib diisi.',
'tanggal.required' => 'Tanggal tayang wajib diisi.',
]; ];
} }
} }

View File

@ -1,115 +0,0 @@
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
class MigrateExistingEmbeddings implements ShouldQueue, ShouldBeUnique
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $timeout = 3600;
public $uniqueFor = 300;
public function handle(): void
{
Log::info("Memulai migrasi embedding untuk data wajah yang sudah ada...");
$users = DB::table('data_wajah')
->whereNotNull('path_video')
->whereNull('face_embeddings')
->get();
if ($users->isEmpty()) {
Log::info("Tidak ada data wajah yang perlu dimigrasi.");
app(\App\Services\NotifikasiService::class)->kirimKeRole(
'hrd',
'pengumuman',
'Migrasi Embedding Selesai',
'Semua data wajah sudah memiliki embedding. Tidak ada yang perlu dimigrasi.'
);
return;
}
Log::info("Ditemukan {$users->count()} user yang perlu dimigrasi.");
$successCount = 0;
$failCount = 0;
foreach ($users as $data) {
$userId = $data->id_user;
$videoPath = Storage::disk('local')->path($data->path_video);
if (!file_exists($videoPath)) {
Log::warning("Migrasi: Video tidak ditemukan untuk user {$userId}: {$videoPath}");
$failCount++;
continue;
}
try {
$response = Http::timeout(180)
->withHeaders([
'X-API-Key' => config('services.flask.api_key'),
])
->attach(
'video',
fopen($videoPath, 'r'),
'enrollment.mp4'
)
->post(config('services.flask.url') . '/extract-and-embed', [
'user_id' => (string) $userId,
'target_frames' => 200,
]);
if (!$response->successful()) {
throw new \Exception("Flask API error: HTTP {$response->status()} - " . $response->body());
}
$output = $response->json();
if (!isset($output['status']) || $output['status'] !== 'success') {
throw new \Exception("Extract & embed error: " . ($output['message'] ?? 'Unknown'));
}
$embedding = $output['embedding'] ?? null;
$jumlahFrame = $output['total_extracted'] ?? 0;
if ($embedding && is_array($embedding)) {
DB::table('data_wajah')->where('id_user', $userId)->update([
'face_embeddings' => json_encode($embedding),
'jumlah_embedding' => $jumlahFrame,
'jumlah_frame' => $jumlahFrame,
'embedding_generated_at' => now(),
]);
Log::info("Migrasi berhasil untuk user {$userId}. Embedding dimensi: " . count($embedding));
$successCount++;
} else {
throw new \Exception("Embedding kosong dari Flask API.");
}
} catch (\Exception $e) {
Log::error("Migrasi GAGAL untuk user {$userId}: " . $e->getMessage());
$failCount++;
}
}
Log::info("Migrasi embedding selesai. Berhasil: {$successCount}, Gagal: {$failCount}");
app(\App\Services\NotifikasiService::class)->kirimKeRole(
'hrd',
'pengumuman',
'Migrasi Embedding Selesai',
"Proses migrasi data wajah ke sistem baru selesai. Berhasil: {$successCount}, Gagal: {$failCount}."
);
}
}

View File

@ -134,9 +134,11 @@ public function handle(): void
app(\App\Services\NotifikasiService::class)->kirimKeRole( app(\App\Services\NotifikasiService::class)->kirimKeRole(
'hrd', 'hrd',
'pengumuman', 'training',
'Sync & Retrain Selesai', 'Sync & Retrain Selesai',
"Sync frame selesai (Extract: {$extractCount}, Skip: {$skipCount}, Gagal: {$failCount}). Model SVM sedang di-training ulang." "Sync frame selesai (Extract: {$extractCount}, Skip: {$skipCount}, Gagal: {$failCount}). Model SVM sedang di-training ulang.",
[],
false
); );
} }
} }

View File

@ -72,9 +72,11 @@ public function handle(): void
app(\App\Services\NotifikasiService::class)->kirimKeRole( app(\App\Services\NotifikasiService::class)->kirimKeRole(
'hrd', 'hrd',
'pengumuman', 'training',
'Training Model Selesai', 'Training Model Selesai',
"Sistem telah berhasil memperbarui model AI Face Recognition untuk {$totalUsers} karyawan." "Sistem telah berhasil memperbarui model AI Face Recognition untuk {$totalUsers} karyawan.",
[],
false
); );
} catch (\Exception $e) { } catch (\Exception $e) {
@ -87,9 +89,11 @@ public function handle(): void
app(\App\Services\NotifikasiService::class)->kirimKeRole( app(\App\Services\NotifikasiService::class)->kirimKeRole(
'hrd', 'hrd',
'pengumuman', 'training',
'Training Model Gagal', 'Training Model Gagal',
'Terjadi kesalahan saat melatih ulang model AI. Silakan cek log server.' 'Terjadi kesalahan saat melatih ulang model AI. Silakan cek log server.',
[],
false
); );
} }
} }

View File

@ -5,26 +5,13 @@
/** /**
* @property int $id * @property int $id
* @property int $id_user * @property int $id_user
* @property string|null $path_model_yml * @property string|null $path_video
* @property int|null $jumlah_frame
* @property int $is_verified 0:Pending, 1:Approved, 2:Rejected * @property int $is_verified 0:Pending, 1:Approved, 2:Rejected
* @property string|null $last_updated * @property string|null $last_updated
* @property string|null $encoding_wajah
* @property string $tanggal_latih
* @property \Illuminate\Support\Carbon|null $created_at * @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at * @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\User $user * @property-read \App\Models\User $user
* @method static \Illuminate\Database\Eloquent\Builder<static>|DataWajah newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|DataWajah newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|DataWajah query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|DataWajah whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DataWajah whereEncodingWajah($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DataWajah whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DataWajah whereIdUser($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DataWajah whereIsVerified($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DataWajah whereLastUpdated($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DataWajah wherePathModelYml($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DataWajah whereTanggalLatih($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DataWajah whereUpdatedAt($value)
* @mixin \Eloquent * @mixin \Eloquent
*/ */
class DataWajah extends Model class DataWajah extends Model

View File

@ -1,30 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
/**
* @property-read \App\Models\User|null $user
* @method static \Illuminate\Database\Eloquent\Builder<static>|Poin newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Poin newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Poin query()
* @mixin \Eloquent
*/
class Poin extends Model
{
protected $table = 'poin';
protected $primaryKey = 'id_poin';
protected $fillable = [
'id_user',
'jumlah_poin',
'sumber',
'tgl_kadaluarsa',
];
public function user()
{
return $this->belongsTo(User::class, 'id_user', 'id');
}
}

View File

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

View File

@ -33,10 +33,10 @@ public function enrollFace($userId, UploadedFile $video)
$existing = DB::table('data_wajah')->where('id_user', $userId)->first(); $existing = DB::table('data_wajah')->where('id_user', $userId)->first();
if ($existing && $existing->is_verified == StatusVerifikasiWajah::APPROVED) { if ($existing && $existing->is_verified == StatusVerifikasiWajah::APPROVED) {
if ($existing->face_embeddings !== null) { if (($existing->jumlah_frame ?? 0) > 0) {
throw new \Exception('Wajah Anda sudah terverifikasi. Hubungi HRD untuk reset.', 400); throw new \Exception('Wajah Anda sudah terverifikasi. Hubungi HRD untuk reset.', 400);
} }
Log::warning("User {$userId} is_verified=APPROVED tapi embedding kosong, izinkan re-enrollment."); Log::warning("User {$userId} is_verified=APPROVED tapi data kosong, izinkan re-enrollment.");
} }
$videoStoragePath = "face_videos/{$userId}"; $videoStoragePath = "face_videos/{$userId}";
@ -51,14 +51,8 @@ public function enrollFace($userId, UploadedFile $video)
DB::table('data_wajah')->updateOrInsert( DB::table('data_wajah')->updateOrInsert(
['id_user' => $userId], ['id_user' => $userId],
[ [
'path_model_yml' => null,
'path_model_pkl' => null,
'path_scaler_pkl' => null,
'path_video' => "{$videoStoragePath}/enrollment.mp4", 'path_video' => "{$videoStoragePath}/enrollment.mp4",
'jumlah_frame' => null, 'jumlah_frame' => null,
'face_embeddings' => null,
'jumlah_embedding' => null,
'embedding_generated_at' => null,
'is_verified' => StatusVerifikasiWajah::PENDING, 'is_verified' => StatusVerifikasiWajah::PENDING,
'last_updated' => now(), 'last_updated' => now(),
] ]

View File

@ -13,7 +13,7 @@ class NotifikasiService
/** /**
* Kirim notifikasi ke satu user. * Kirim notifikasi ke satu user.
*/ */
public function kirim(int $idUser, string $tipe, string $judul, string $pesan, array $data = []): void public function kirim(int $idUser, string $tipe, string $judul, string $pesan, array $data = [], bool $sendFcm = true): void
{ {
try { try {
Notifikasi::create([ Notifikasi::create([
@ -28,8 +28,10 @@ public function kirim(int $idUser, string $tipe, string $judul, string $pesan, a
$unreadCount = Notifikasi::where('id_user', $idUser)->where('is_read', false)->count(); $unreadCount = Notifikasi::where('id_user', $idUser)->where('is_read', false)->count();
broadcast(new NotifikasiCreated($idUser, $judul, $pesan, $tipe, $unreadCount)); broadcast(new NotifikasiCreated($idUser, $judul, $pesan, $tipe, $unreadCount));
if ($sendFcm) {
$data['tipe'] = $tipe; $data['tipe'] = $tipe;
\App\Jobs\SendFcmPushNotification::dispatch($idUser, $judul, $pesan, $data); \App\Jobs\SendFcmPushNotification::dispatch($idUser, $judul, $pesan, $data);
}
} catch (\Exception $e) { } catch (\Exception $e) {
Log::error('NotifikasiService::kirim gagal: ' . $e->getMessage()); Log::error('NotifikasiService::kirim gagal: ' . $e->getMessage());
} }
@ -46,7 +48,7 @@ public function kirimBroadcast(string $tipe, string $judul, string $pesan, array
/** /**
* Kirim ke semua user dengan role tertentu (contoh: HRD, Manajer). * Kirim ke semua user dengan role tertentu (contoh: HRD, Manajer).
*/ */
public function kirimKeRole(string $namaRole, string $tipe, string $judul, string $pesan, array $data = []): void public function kirimKeRole(string $namaRole, string $tipe, string $judul, string $pesan, array $data = [], bool $sendFcm = true): void
{ {
$users = User::where('status_aktif', 1) $users = User::where('status_aktif', 1)
->whereHas('roles', function ($q) use ($namaRole) { ->whereHas('roles', function ($q) use ($namaRole) {
@ -55,7 +57,7 @@ public function kirimKeRole(string $namaRole, string $tipe, string $judul, strin
->pluck('id'); ->pluck('id');
foreach ($users as $idUser) { foreach ($users as $idUser) {
$this->kirim($idUser, $tipe, $judul, $pesan, $data); $this->kirim($idUser, $tipe, $judul, $pesan, $data, $sendFcm);
} }
} }

View File

@ -64,6 +64,10 @@ public function getDynamicSchedule($userId, $date)
public function absenMasuk($user, $request) public function absenMasuk($user, $request)
{ {
if (!$user->is_face_registered || !$user->dataWajah || $user->dataWajah->is_verified != StatusVerifikasiWajah::APPROVED) {
throw new \Exception('Presensi ditolak. Wajah Anda belum terdaftar atau belum disetujui/diverifikasi oleh HRD.', 403);
}
$hariIni = Carbon::today('Asia/Jakarta')->toDateString(); $hariIni = Carbon::today('Asia/Jakarta')->toDateString();
$jamSekarang = Carbon::now('Asia/Jakarta'); $jamSekarang = Carbon::now('Asia/Jakarta');
@ -180,6 +184,10 @@ public function absenMasuk($user, $request)
public function absenPulang($user, $request) public function absenPulang($user, $request)
{ {
if (!$user->is_face_registered || !$user->dataWajah || $user->dataWajah->is_verified != StatusVerifikasiWajah::APPROVED) {
throw new \Exception('Presensi ditolak. Wajah Anda belum terdaftar atau belum disetujui/diverifikasi oleh HRD.', 403);
}
$jamSekarang = Carbon::now('Asia/Jakarta'); $jamSekarang = Carbon::now('Asia/Jakarta');
$presensi = Presensi::where('id_user', $user->id) $presensi = Presensi::where('id_user', $user->id)

View File

@ -13,6 +13,7 @@
health: '/up', health: '/up',
) )
->withMiddleware(function (Middleware $middleware): void { ->withMiddleware(function (Middleware $middleware): void {
$middleware->trustProxies(at: '*');
$middleware->alias([ $middleware->alias([
'role' => \App\Http\Middleware\CheckRole::class, 'role' => \App\Http\Middleware\CheckRole::class,
]); ]);

View File

@ -53,6 +53,10 @@
"Composer\\Config::disableProcessTimeout", "Composer\\Config::disableProcessTimeout",
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74,#a3e635\" \"php artisan serve --host=0.0.0.0\" \"php artisan queue:listen --tries=1 --timeout=0\" \"php artisan reverb:start\" \"npm run dev\" --names=server,queue,reverb,vite --kill-others" "npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74,#a3e635\" \"php artisan serve --host=0.0.0.0\" \"php artisan queue:listen --tries=1 --timeout=0\" \"php artisan reverb:start\" \"npm run dev\" --names=server,queue,reverb,vite --kill-others"
], ],
"serve-all": [
"Composer\\Config::disableProcessTimeout",
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185\" \"php artisan serve --host=0.0.0.0\" \"php artisan queue:listen --tries=1 --timeout=0\" \"php artisan reverb:start\" --names=server,queue,reverb --kill-others"
],
"test": [ "test": [
"@php artisan config:clear --ansi", "@php artisan config:clear --ansi",
"@php artisan test" "@php artisan test"

View File

@ -22,6 +22,8 @@
'key' => env('APP_KEY'), 'key' => env('APP_KEY'),
'cron_secret_key' => env('CRON_SECRET_KEY', ''),
'previous_keys' => [ 'previous_keys' => [
...array_filter( ...array_filter(
explode(',', (string) env('APP_PREVIOUS_KEYS', '')) explode(',', (string) env('APP_PREVIOUS_KEYS', ''))

1197
database/database_export.sql Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@ -0,0 +1,802 @@
-- MySQL dump 10.19 Distrib 10.4.32-MySQL, for Win64 (AMD64)
--
-- Host: localhost Database: db_mpg
-- ------------------------------------------------------
-- Server version 10.4.32-MySQL
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
--
-- Table structure for table `approval_surat`
--
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `approval_surat` (
`id_approval` varchar(20) NOT NULL,
`id_surat` varchar(20) NOT NULL,
`id_approver` bigint(20) unsigned NOT NULL,
`id_ttd_approver` varchar(20) DEFAULT NULL,
`tahap` tinyint(4) NOT NULL COMMENT '1=Manajer, 2=HRD',
`status` enum('disetujui','ditolak') NOT NULL DEFAULT 'disetujui',
`catatan` text DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id_approval`),
UNIQUE KEY `approval_surat_id_surat_tahap_unique` (`id_surat`,`tahap`),
KEY `approval_surat_id_approver_foreign` (`id_approver`),
KEY `approval_surat_id_ttd_approver_foreign` (`id_ttd_approver`),
CONSTRAINT `approval_surat_id_approver_foreign` FOREIGN KEY (`id_approver`) REFERENCES `users` (`id`),
CONSTRAINT `approval_surat_id_surat_foreign` FOREIGN KEY (`id_surat`) REFERENCES `surat_izin` (`id_surat`) ON DELETE CASCADE,
CONSTRAINT `approval_surat_id_ttd_approver_foreign` FOREIGN KEY (`id_ttd_approver`) REFERENCES `tanda_tangan` (`id_tanda_tangan`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `cache`
--
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `cache` (
`key` varchar(255) NOT NULL,
`value` mediumtext NOT NULL,
`expiration` int(11) NOT NULL,
PRIMARY KEY (`key`),
KEY `cache_expiration_index` (`expiration`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `cache_locks`
--
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `cache_locks` (
`key` varchar(255) NOT NULL,
`owner` varchar(255) NOT NULL,
`expiration` int(11) NOT NULL,
PRIMARY KEY (`key`),
KEY `cache_locks_expiration_index` (`expiration`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `data_wajah`
--
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `data_wajah` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`id_user` bigint(20) unsigned NOT NULL,
`path_video` varchar(255) DEFAULT NULL,
`jumlah_frame` int(11) DEFAULT NULL,
`is_verified` tinyint(4) NOT NULL DEFAULT 0 COMMENT '0:Pending, 1:Approved, 2:Rejected',
`last_updated` timestamp NULL DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `data_wajah_id_user_foreign` (`id_user`),
CONSTRAINT `data_wajah_id_user_foreign` FOREIGN KEY (`id_user`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `detail_penggunaan_poin`
--
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `detail_penggunaan_poin` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`id_penggunaan` varchar(20) NOT NULL,
`id_poin_sumber` bigint(20) unsigned NOT NULL,
`jumlah_diambil` int(11) NOT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `detail_penggunaan_poin_id_poin_sumber_foreign` (`id_poin_sumber`),
KEY `detail_penggunaan_poin_id_penggunaan_foreign` (`id_penggunaan`),
CONSTRAINT `detail_penggunaan_poin_id_penggunaan_foreign` FOREIGN KEY (`id_penggunaan`) REFERENCES `penggunaan_poin` (`id_penggunaan`) ON DELETE CASCADE,
CONSTRAINT `detail_penggunaan_poin_id_poin_sumber_foreign` FOREIGN KEY (`id_poin_sumber`) REFERENCES `poin_lembur` (`id_poin`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `detail_role_permissions`
--
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `detail_role_permissions` (
`id_role` bigint(20) unsigned NOT NULL,
`id_permission` bigint(20) unsigned NOT NULL,
PRIMARY KEY (`id_role`,`id_permission`),
KEY `detail_role_permissions_id_permission_foreign` (`id_permission`),
CONSTRAINT `detail_role_permissions_id_permission_foreign` FOREIGN KEY (`id_permission`) REFERENCES `permissions` (`id_permission`) ON DELETE CASCADE,
CONSTRAINT `detail_role_permissions_id_role_foreign` FOREIGN KEY (`id_role`) REFERENCES `roles` (`id_role`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `detail_user_roles`
--
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `detail_user_roles` (
`id_user` bigint(20) unsigned NOT NULL,
`id_role` bigint(20) unsigned NOT NULL,
PRIMARY KEY (`id_user`,`id_role`),
KEY `detail_user_roles_id_role_foreign` (`id_role`),
CONSTRAINT `detail_user_roles_id_role_foreign` FOREIGN KEY (`id_role`) REFERENCES `roles` (`id_role`) ON DELETE CASCADE,
CONSTRAINT `detail_user_roles_id_user_foreign` FOREIGN KEY (`id_user`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `device_tokens`
--
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `device_tokens` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`id_user` bigint(20) unsigned NOT NULL,
`fcm_token` varchar(255) NOT NULL,
`device_type` varchar(20) NOT NULL DEFAULT 'android',
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `device_tokens_fcm_token_unique` (`fcm_token`),
KEY `device_tokens_id_user_index` (`id_user`),
CONSTRAINT `device_tokens_id_user_foreign` FOREIGN KEY (`id_user`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `divisi`
--
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `divisi` (
`id_divisi` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`nama_divisi` varchar(50) NOT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id_divisi`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `failed_jobs`
--
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `failed_jobs` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`uuid` varchar(255) NOT NULL,
`connection` text NOT NULL,
`queue` text NOT NULL,
`payload` longtext NOT NULL,
`exception` longtext NOT NULL,
`failed_at` timestamp NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`),
UNIQUE KEY `failed_jobs_uuid_unique` (`uuid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `hari_libur`
--
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `hari_libur` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`tanggal` date NOT NULL,
`keterangan` varchar(255) NOT NULL,
`id_kantor` bigint(20) unsigned DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `hari_libur_id_kantor_foreign` (`id_kantor`),
CONSTRAINT `hari_libur_id_kantor_foreign` FOREIGN KEY (`id_kantor`) REFERENCES `kantor` (`id_kantor`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `jabatan`
--
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `jabatan` (
`id_jabatan` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`nama_jabatan` varchar(50) NOT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id_jabatan`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `jadwal_kerja`
--
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `jadwal_kerja` (
`id_jadwal` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`id_user` bigint(20) unsigned DEFAULT NULL,
`tanggal` date DEFAULT NULL,
`id_shift` bigint(20) unsigned DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id_jadwal`),
KEY `jadwal_kerja_id_shift_foreign` (`id_shift`),
KEY `idx_jadwal_tanggal` (`tanggal`),
KEY `jadwal_kerja_id_user_tanggal_index` (`id_user`,`tanggal`),
CONSTRAINT `jadwal_kerja_id_shift_foreign` FOREIGN KEY (`id_shift`) REFERENCES `shift_kerja` (`id_shift`),
CONSTRAINT `jadwal_kerja_id_user_foreign` FOREIGN KEY (`id_user`) REFERENCES `users` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `jenis_izin`
--
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `jenis_izin` (
`id_jenis_izin` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`nama_izin` varchar(30) NOT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id_jenis_izin`),
UNIQUE KEY `jenis_izin_nama_izin_unique` (`nama_izin`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `jenis_kompensasi`
--
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `jenis_kompensasi` (
`id_kompensasi` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`nama_kompensasi` varchar(30) NOT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id_kompensasi`),
UNIQUE KEY `jenis_kompensasi_nama_kompensasi_unique` (`nama_kompensasi`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `jenis_pengurangan`
--
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `jenis_pengurangan` (
`id_pengurangan` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`nama_pengurangan` varchar(30) NOT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id_pengurangan`),
UNIQUE KEY `jenis_pengurangan_nama_pengurangan_unique` (`nama_pengurangan`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `jobs`
--
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `jobs` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`queue` varchar(255) NOT NULL,
`payload` longtext NOT NULL,
`attempts` tinyint(3) unsigned NOT NULL,
`reserved_at` int(10) unsigned DEFAULT NULL,
`available_at` int(10) unsigned NOT NULL,
`created_at` int(10) unsigned NOT NULL,
PRIMARY KEY (`id`),
KEY `jobs_queue_reserved_at_available_at_index` (`queue`,`reserved_at`,`available_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `kantor`
--
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `kantor` (
`id_kantor` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`nama_kantor` varchar(50) NOT NULL,
`tipe` varchar(20) NOT NULL DEFAULT 'Cabang',
`alamat` varchar(150) DEFAULT NULL,
`latitude` decimal(10,8) DEFAULT NULL,
`longitude` decimal(11,8) DEFAULT NULL,
`radius` int(11) NOT NULL DEFAULT 100,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id_kantor`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `lembur`
--
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `lembur` (
`id_lembur` varchar(20) NOT NULL,
`id_user` bigint(20) unsigned DEFAULT NULL,
`tanggal_lembur` date NOT NULL,
`jam_mulai` time NOT NULL,
`jam_selesai` time NOT NULL,
`durasi_menit` int(11) DEFAULT NULL,
`keterangan` varchar(255) DEFAULT NULL,
`jumlah_poin` int(11) DEFAULT NULL,
`id_kompensasi` bigint(20) unsigned DEFAULT NULL,
`id_status` bigint(20) unsigned NOT NULL DEFAULT 1,
`alasan_penolakan` varchar(150) DEFAULT NULL,
`tanggal_diajukan` timestamp NOT NULL DEFAULT current_timestamp(),
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id_lembur`),
KEY `lembur_id_kompensasi_foreign` (`id_kompensasi`),
KEY `lembur_id_status_index` (`id_status`),
KEY `lembur_id_user_index` (`id_user`),
CONSTRAINT `lembur_id_kompensasi_foreign` FOREIGN KEY (`id_kompensasi`) REFERENCES `jenis_kompensasi` (`id_kompensasi`),
CONSTRAINT `lembur_id_status_foreign` FOREIGN KEY (`id_status`) REFERENCES `status_pengajuan` (`id_status`),
CONSTRAINT `lembur_id_user_foreign` FOREIGN KEY (`id_user`) REFERENCES `users` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `log_verifikasi_wajah`
--
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `log_verifikasi_wajah` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`id_user` bigint(20) unsigned NOT NULL,
`skor_kepercayaan` decimal(10,4) DEFAULT NULL,
`skor_svm` decimal(10,4) DEFAULT NULL,
`jarak_normalisasi` decimal(8,4) DEFAULT NULL,
`status_verifikasi` varchar(30) DEFAULT NULL,
`apakah_cocok` tinyint(1) NOT NULL DEFAULT 0,
`skor_blur` decimal(6,1) DEFAULT NULL,
`tipe` varchar(10) NOT NULL DEFAULT 'presensi',
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `log_verifikasi_wajah_id_user_created_at_index` (`id_user`,`created_at`),
CONSTRAINT `log_verifikasi_wajah_id_user_foreign` FOREIGN KEY (`id_user`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `migrations`
--
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `migrations` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`migration` varchar(255) NOT NULL,
`batch` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `notifikasi`
--
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `notifikasi` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`id_user` bigint(20) unsigned NOT NULL,
`judul` varchar(255) NOT NULL,
`pesan` text NOT NULL,
`tipe` varchar(50) NOT NULL,
`data` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
`is_read` tinyint(1) NOT NULL DEFAULT 0,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `notifikasi_id_user_is_read_index` (`id_user`,`is_read`),
KEY `notifikasi_id_user_created_at_index` (`id_user`,`created_at`),
CONSTRAINT `notifikasi_id_user_foreign` FOREIGN KEY (`id_user`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `pengajuan_izin`
--
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `pengajuan_izin` (
`id_izin` varchar(20) NOT NULL,
`id_user` bigint(20) unsigned DEFAULT NULL,
`tanggal_mulai` date NOT NULL,
`tanggal_selesai` date NOT NULL,
`id_jenis_izin` bigint(20) unsigned DEFAULT NULL,
`alasan` varchar(150) DEFAULT NULL,
`bukti_file` varchar(150) DEFAULT NULL,
`id_status` bigint(20) unsigned NOT NULL DEFAULT 1,
`alasan_penolakan` varchar(150) DEFAULT NULL,
`tanggal_diajukan` timestamp NOT NULL DEFAULT current_timestamp(),
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id_izin`),
KEY `pengajuan_izin_id_jenis_izin_foreign` (`id_jenis_izin`),
KEY `pengajuan_izin_id_status_index` (`id_status`),
KEY `pengajuan_izin_id_user_index` (`id_user`),
CONSTRAINT `pengajuan_izin_id_jenis_izin_foreign` FOREIGN KEY (`id_jenis_izin`) REFERENCES `jenis_izin` (`id_jenis_izin`),
CONSTRAINT `pengajuan_izin_id_status_foreign` FOREIGN KEY (`id_status`) REFERENCES `status_pengajuan` (`id_status`),
CONSTRAINT `pengajuan_izin_id_user_foreign` FOREIGN KEY (`id_user`) REFERENCES `users` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `penggunaan_poin`
--
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `penggunaan_poin` (
`id_penggunaan` varchar(20) NOT NULL,
`id_user` bigint(20) unsigned DEFAULT NULL,
`tanggal_penggunaan` date NOT NULL,
`jumlah_poin` int(11) DEFAULT NULL,
`jam_masuk_custom` time DEFAULT NULL,
`jam_pulang_custom` time DEFAULT NULL,
`id_pengurangan` bigint(20) unsigned DEFAULT NULL,
`id_status` bigint(20) unsigned NOT NULL DEFAULT 1,
`alasan_penolakan` varchar(150) DEFAULT NULL,
`tanggal_diajukan` timestamp NOT NULL DEFAULT current_timestamp(),
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id_penggunaan`),
KEY `penggunaan_poin_id_pengurangan_foreign` (`id_pengurangan`),
KEY `penggunaan_poin_id_user_tanggal_penggunaan_index` (`id_user`,`tanggal_penggunaan`),
KEY `penggunaan_poin_id_status_index` (`id_status`),
CONSTRAINT `penggunaan_poin_id_pengurangan_foreign` FOREIGN KEY (`id_pengurangan`) REFERENCES `jenis_pengurangan` (`id_pengurangan`),
CONSTRAINT `penggunaan_poin_id_status_foreign` FOREIGN KEY (`id_status`) REFERENCES `status_pengajuan` (`id_status`),
CONSTRAINT `penggunaan_poin_id_user_foreign` FOREIGN KEY (`id_user`) REFERENCES `users` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `pengumuman`
--
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `pengumuman` (
`id_pengumuman` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`judul` varchar(80) DEFAULT NULL,
`isi` varchar(500) DEFAULT NULL,
`foto` varchar(255) DEFAULT NULL,
`lampiran` varchar(255) DEFAULT NULL,
`tanggal` date DEFAULT NULL,
`dibuat_oleh` bigint(20) unsigned DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id_pengumuman`),
KEY `pengumuman_dibuat_oleh_foreign` (`dibuat_oleh`),
CONSTRAINT `pengumuman_dibuat_oleh_foreign` FOREIGN KEY (`dibuat_oleh`) REFERENCES `users` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `permissions`
--
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `permissions` (
`id_permission` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`nama_permission` varchar(255) NOT NULL,
`slug` varchar(255) NOT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id_permission`),
UNIQUE KEY `permissions_nama_permission_unique` (`nama_permission`),
UNIQUE KEY `permissions_slug_unique` (`slug`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `personal_access_tokens`
--
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `personal_access_tokens` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`tokenable_type` varchar(255) NOT NULL,
`tokenable_id` bigint(20) unsigned NOT NULL,
`name` text NOT NULL,
`token` varchar(64) NOT NULL,
`abilities` text DEFAULT NULL,
`last_used_at` timestamp NULL DEFAULT NULL,
`expires_at` timestamp NULL DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `personal_access_tokens_token_unique` (`token`),
KEY `personal_access_tokens_tokenable_type_tokenable_id_index` (`tokenable_type`,`tokenable_id`),
KEY `personal_access_tokens_expires_at_index` (`expires_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `poin_lembur`
--
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `poin_lembur` (
`id_poin` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`id_user` bigint(20) unsigned DEFAULT NULL,
`jumlah_poin` int(11) DEFAULT NULL,
`sisa_poin` int(11) DEFAULT NULL,
`id_lembur` varchar(20) DEFAULT NULL,
`keterangan` varchar(150) DEFAULT NULL,
`tanggal` timestamp NOT NULL DEFAULT current_timestamp(),
`expired_at` date DEFAULT NULL,
`is_fully_used` tinyint(1) DEFAULT 0,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id_poin`),
KEY `poin_lembur_id_user_index` (`id_user`),
KEY `poin_lembur_tanggal_index` (`tanggal`),
KEY `poin_lembur_id_lembur_foreign` (`id_lembur`),
CONSTRAINT `poin_lembur_id_lembur_foreign` FOREIGN KEY (`id_lembur`) REFERENCES `lembur` (`id_lembur`) ON DELETE SET NULL,
CONSTRAINT `poin_lembur_id_user_foreign` FOREIGN KEY (`id_user`) REFERENCES `users` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `presensi`
--
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `presensi` (
`id_presensi` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`id_user` bigint(20) unsigned DEFAULT NULL,
`tanggal` date NOT NULL,
`jam_masuk` time DEFAULT NULL,
`jam_pulang` time DEFAULT NULL,
`lat_masuk` decimal(10,8) DEFAULT NULL,
`lon_masuk` decimal(11,8) DEFAULT NULL,
`lat_pulang` decimal(10,8) DEFAULT NULL,
`lon_pulang` decimal(11,8) DEFAULT NULL,
`foto_wajah_masuk` varchar(255) DEFAULT NULL,
`foto_wajah_pulang` varchar(255) DEFAULT NULL,
`id_status` bigint(20) unsigned DEFAULT NULL,
`alasan_telat` varchar(150) DEFAULT NULL,
`keterangan_pulang` varchar(150) DEFAULT NULL,
`waktu_terlambat` time DEFAULT NULL,
`waktu_masuk_awal` time DEFAULT NULL,
`waktu_pulang_awal` time DEFAULT NULL,
`waktu_pulang_akhir` time DEFAULT NULL,
`verifikasi_wajah` tinyint(1) NOT NULL DEFAULT 0,
`id_validasi` bigint(20) unsigned NOT NULL DEFAULT 1,
`keterangan_luar_radius` text DEFAULT NULL,
`alasan_penolakan` varchar(150) DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
`deleted_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id_presensi`),
KEY `idx_presensi_tanggal` (`tanggal`),
KEY `presensi_tanggal_index` (`tanggal`),
KEY `presensi_id_user_index` (`id_user`),
KEY `presensi_id_status_index` (`id_status`),
KEY `presensi_id_validasi_index` (`id_validasi`),
KEY `presensi_id_user_tanggal_index` (`id_user`,`tanggal`),
CONSTRAINT `presensi_id_user_foreign` FOREIGN KEY (`id_user`) REFERENCES `users` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `riwayat_tukar_shift`
--
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `riwayat_tukar_shift` (
`id_riwayat` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`id_user_1` bigint(20) unsigned NOT NULL,
`id_jadwal_1` bigint(20) unsigned NOT NULL,
`id_user_2` bigint(20) unsigned NOT NULL,
`id_jadwal_2` bigint(20) unsigned NOT NULL,
`keterangan` text DEFAULT NULL,
`created_by` bigint(20) unsigned NOT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id_riwayat`),
KEY `riwayat_tukar_shift_id_user_1_foreign` (`id_user_1`),
KEY `riwayat_tukar_shift_id_jadwal_1_foreign` (`id_jadwal_1`),
KEY `riwayat_tukar_shift_id_user_2_foreign` (`id_user_2`),
KEY `riwayat_tukar_shift_id_jadwal_2_foreign` (`id_jadwal_2`),
KEY `riwayat_tukar_shift_created_by_foreign` (`created_by`),
CONSTRAINT `riwayat_tukar_shift_created_by_foreign` FOREIGN KEY (`created_by`) REFERENCES `users` (`id`) ON DELETE CASCADE,
CONSTRAINT `riwayat_tukar_shift_id_jadwal_1_foreign` FOREIGN KEY (`id_jadwal_1`) REFERENCES `jadwal_kerja` (`id_jadwal`) ON DELETE CASCADE,
CONSTRAINT `riwayat_tukar_shift_id_jadwal_2_foreign` FOREIGN KEY (`id_jadwal_2`) REFERENCES `jadwal_kerja` (`id_jadwal`) ON DELETE CASCADE,
CONSTRAINT `riwayat_tukar_shift_id_user_1_foreign` FOREIGN KEY (`id_user_1`) REFERENCES `users` (`id`) ON DELETE CASCADE,
CONSTRAINT `riwayat_tukar_shift_id_user_2_foreign` FOREIGN KEY (`id_user_2`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `roles`
--
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `roles` (
`id_role` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`nama_role` varchar(30) NOT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id_role`),
UNIQUE KEY `roles_nama_role_unique` (`nama_role`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `shift_kerja`
--
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `shift_kerja` (
`id_shift` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`nama_shift` varchar(50) DEFAULT NULL,
`jam_mulai` time DEFAULT NULL,
`jam_selesai` time DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id_shift`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `status_pengajuan`
--
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `status_pengajuan` (
`id_status` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`nama_status` varchar(30) NOT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id_status`),
UNIQUE KEY `status_pengajuan_nama_status_unique` (`nama_status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `surat_izin`
--
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `surat_izin` (
`id_surat` varchar(20) NOT NULL,
`id_izin` varchar(20) NOT NULL,
`id_user` bigint(20) unsigned NOT NULL,
`nomor_surat` varchar(50) NOT NULL,
`isi_surat` text NOT NULL,
`id_ttd_pengaju` varchar(20) DEFAULT NULL,
`status_surat` enum('menunggu_manajer','menunggu_hrd','disetujui','ditolak') NOT NULL DEFAULT 'menunggu_manajer',
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id_surat`),
KEY `surat_izin_id_izin_foreign` (`id_izin`),
KEY `surat_izin_id_ttd_pengaju_foreign` (`id_ttd_pengaju`),
KEY `surat_izin_id_user_status_surat_index` (`id_user`,`status_surat`),
CONSTRAINT `surat_izin_id_izin_foreign` FOREIGN KEY (`id_izin`) REFERENCES `pengajuan_izin` (`id_izin`) ON DELETE CASCADE,
CONSTRAINT `surat_izin_id_ttd_pengaju_foreign` FOREIGN KEY (`id_ttd_pengaju`) REFERENCES `tanda_tangan` (`id_tanda_tangan`) ON DELETE SET NULL,
CONSTRAINT `surat_izin_id_user_foreign` FOREIGN KEY (`id_user`) REFERENCES `users` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `tanda_tangan`
--
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `tanda_tangan` (
`id_tanda_tangan` varchar(20) NOT NULL,
`id_user` bigint(20) unsigned NOT NULL,
`file_ttd` varchar(255) NOT NULL,
`is_active` tinyint(1) NOT NULL DEFAULT 1,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id_tanda_tangan`),
KEY `tanda_tangan_id_user_is_active_index` (`id_user`,`is_active`),
CONSTRAINT `tanda_tangan_id_user_foreign` FOREIGN KEY (`id_user`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `users`
--
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `users` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`nik` varchar(255) DEFAULT NULL,
`nama_lengkap` varchar(255) NOT NULL,
`email` varchar(255) NOT NULL,
`no_telp` varchar(255) DEFAULT NULL,
`alamat` text DEFAULT NULL,
`email_verified_at` timestamp NULL DEFAULT NULL,
`password` varchar(255) NOT NULL,
`sisa_cuti` int(11) NOT NULL DEFAULT 12,
`foto` varchar(255) DEFAULT NULL,
`remember_token` varchar(100) DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
`id_kantor` bigint(20) unsigned DEFAULT NULL,
`id_divisi` bigint(20) unsigned DEFAULT NULL,
`id_jabatan` bigint(20) unsigned DEFAULT NULL,
`status_aktif` tinyint(1) NOT NULL DEFAULT 1,
`tgl_bergabung` date DEFAULT NULL,
`is_face_registered` tinyint(1) NOT NULL DEFAULT 0,
`deleted_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `users_email_unique` (`email`),
UNIQUE KEY `users_nik_unique` (`nik`),
KEY `users_id_jabatan_index` (`id_jabatan`),
KEY `users_id_divisi_index` (`id_divisi`),
KEY `users_id_kantor_index` (`id_kantor`),
KEY `users_status_aktif_index` (`status_aktif`),
CONSTRAINT `users_id_divisi_foreign` FOREIGN KEY (`id_divisi`) REFERENCES `divisi` (`id_divisi`) ON DELETE SET NULL,
CONSTRAINT `users_id_jabatan_foreign` FOREIGN KEY (`id_jabatan`) REFERENCES `jabatan` (`id_jabatan`) ON DELETE SET NULL,
CONSTRAINT `users_id_kantor_foreign` FOREIGN KEY (`id_kantor`) REFERENCES `kantor` (`id_kantor`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- Dump completed on 2026-05-17 23:31:48

View File

@ -0,0 +1,16 @@
-- =====================================================
-- Cleanup kolom deprecated di tabel data_wajah
-- Jalankan di MySQL Workbench / phpMyAdmin / CLI
-- =====================================================
USE `db_mpg`;
ALTER TABLE `data_wajah`
DROP COLUMN IF EXISTS `path_model_yml`,
DROP COLUMN IF EXISTS `path_model_pkl`,
DROP COLUMN IF EXISTS `path_scaler_pkl`,
DROP COLUMN IF EXISTS `face_embeddings`,
DROP COLUMN IF EXISTS `jumlah_embedding`,
DROP COLUMN IF EXISTS `embedding_generated_at`,
DROP COLUMN IF EXISTS `encoding_wajah`,
DROP COLUMN IF EXISTS `tanggal_latih`;

View File

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('presensi', function (Blueprint $table) {
$table->dropForeign(['id_status']);
$table->dropForeign(['id_validasi']);
});
Schema::dropIfExists('status_presensi');
Schema::dropIfExists('status_validasi_presensi');
}
/**
* Reverse the migrations.
*/
public function down(): void
{
// Recreate tables and constraints if needed, but omitted since they are replaced by Enum
}
};

View File

@ -0,0 +1,60 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('data_wajah', function (Blueprint $table) {
$columnsToDrop = [
'path_model_yml',
'encoding_wajah',
'tanggal_latih',
'path_model_pkl',
'path_scaler_pkl',
'face_embeddings',
'jumlah_embedding',
'embedding_generated_at',
];
foreach ($columnsToDrop as $column) {
if (Schema::hasColumn('data_wajah', $column)) {
$table->dropColumn($column);
}
}
});
}
public function down(): void
{
Schema::table('data_wajah', function (Blueprint $table) {
if (!Schema::hasColumn('data_wajah', 'path_model_yml')) {
$table->string('path_model_yml')->nullable();
}
if (!Schema::hasColumn('data_wajah', 'encoding_wajah')) {
$table->text('encoding_wajah')->nullable();
}
if (!Schema::hasColumn('data_wajah', 'tanggal_latih')) {
$table->timestamp('tanggal_latih')->useCurrent();
}
if (!Schema::hasColumn('data_wajah', 'path_model_pkl')) {
$table->string('path_model_pkl')->nullable();
}
if (!Schema::hasColumn('data_wajah', 'path_scaler_pkl')) {
$table->string('path_scaler_pkl')->nullable();
}
if (!Schema::hasColumn('data_wajah', 'face_embeddings')) {
$table->json('face_embeddings')->nullable();
}
if (!Schema::hasColumn('data_wajah', 'jumlah_embedding')) {
$table->integer('jumlah_embedding')->nullable();
}
if (!Schema::hasColumn('data_wajah', 'embedding_generated_at')) {
$table->timestamp('embedding_generated_at')->nullable();
}
});
}
};

BIN
public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

View File

@ -1,3 +1,5 @@
const APP_BASE_URL = document.querySelector('meta[name="app-url"]')?.content || '';
function initSweetAlerts() { function initSweetAlerts() {
const swalData = document.getElementById('swal-data'); const swalData = document.getElementById('swal-data');
const flashError = document.querySelector('.flash-data-error'); const flashError = document.querySelector('.flash-data-error');
@ -91,7 +93,7 @@ document.addEventListener('DOMContentLoaded', initSweetAlerts);
const badge = document.getElementById('notif-badge'); const badge = document.getElementById('notif-badge');
if (!badge) return; // User tidak punya akses notifikasi, hentikan tanpa fetch if (!badge) return; // User tidak punya akses notifikasi, hentikan tanpa fetch
fetch('/notifikasi/unread-count', { fetch(APP_BASE_URL + '/notifikasi/unread-count', {
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' } headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' }
}) })
.then(r => { .then(r => {
@ -129,7 +131,7 @@ function notifDropdown() {
loadRecent() { loadRecent() {
this.loading = true; this.loading = true;
fetch('/notifikasi/recent', { fetch(APP_BASE_URL + '/notifikasi/recent', {
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' } headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' }
}) })
.then(r => r.json()) .then(r => r.json())
@ -143,7 +145,7 @@ function notifDropdown() {
readItem(item) { readItem(item) {
if (!item.is_read) { if (!item.is_read) {
fetch('/notifikasi/' + item.id + '/read', { fetch(APP_BASE_URL + '/notifikasi/' + item.id + '/read', {
method: 'POST', method: 'POST',
headers: { headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content, 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
@ -159,7 +161,7 @@ function notifDropdown() {
}, },
markAllRead() { markAllRead() {
fetch('/notifikasi/read-all', { fetch(APP_BASE_URL + '/notifikasi/read-all', {
method: 'POST', method: 'POST',
headers: { headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content, 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,

View File

@ -3,12 +3,16 @@ import Echo from 'laravel-echo';
import Pusher from 'pusher-js'; import Pusher from 'pusher-js';
window.Pusher = Pusher; window.Pusher = Pusher;
window.Echo = new Echo({ const reverbKey = import.meta.env.VITE_REVERB_APP_KEY;
if (reverbKey) {
window.Echo = new Echo({
broadcaster: 'reverb', broadcaster: 'reverb',
key: import.meta.env.VITE_REVERB_APP_KEY, key: reverbKey,
wsHost: import.meta.env.VITE_REVERB_HOST, wsHost: import.meta.env.VITE_REVERB_HOST,
wsPort: import.meta.env.VITE_REVERB_PORT ?? 80, wsPort: import.meta.env.VITE_REVERB_PORT ?? 80,
wssPort: import.meta.env.VITE_REVERB_PORT ?? 443, wssPort: import.meta.env.VITE_REVERB_PORT ?? 443,
forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https', forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https',
enabledTransports: ['ws', 'wss'], enabledTransports: ['ws', 'wss'],
}); });
}

View File

@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login Administrator - HRIS MP</title> <title>Login Portal HRD & Administrator - HRIS MP</title>
@vite(['resources/css/app.css', 'resources/js/app.js']) @vite(['resources/css/app.css', 'resources/js/app.js'])
@ -100,7 +100,7 @@ class="absolute inset-0 h-full w-full object-cover"
<div class="mb-8 text-center flex flex-col items-center"> <div class="mb-8 text-center flex flex-col items-center">
<h2 class="text-3xl font-extrabold text-[#130F26] tracking-tight">Sign in</h2> <h2 class="text-3xl font-extrabold text-[#130F26] tracking-tight">Sign in</h2>
<p class="mt-2 text-sm text-slate-500"> <p class="mt-2 text-sm text-slate-500">
Masuk menggunakan akun administrator Anda. Masuk menggunakan akun HRD atau Administrator Anda.
</p> </p>
</div> </div>

View File

@ -271,23 +271,9 @@ class="inline-flex items-center px-3 py-1.5 border border-amber-200 text-xs font
</svg> </svg>
Reset Reset
</button> </button>
<button onclick="openUploadModal({{ $user->id }}, '{{ addslashes($user->nama_lengkap) }}')"
class="inline-flex items-center px-3 py-1.5 border border-blue-200 text-xs font-medium rounded-lg text-blue-700 bg-blue-50 hover:bg-blue-100 transition-colors shadow-sm"
title="Upload Ulang Video Wajah">
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"></path>
</svg>
Re-Upload
</button>
@else @else
<button onclick="openUploadModal({{ $user->id }}, '{{ addslashes($user->nama_lengkap) }}')" <span class="text-xs text-slate-400 italic">Registrasi via Aplikasi</span>
class="inline-flex items-center px-3 py-1.5 border border-slate-200 text-xs font-medium rounded-lg text-slate-700 bg-white hover:bg-slate-50 transition-colors shadow-sm"
title="Upload Video Wajah">
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"></path>
</svg>
Upload Video
</button>
@endif @endif
</div> </div>
</td> </td>
@ -319,80 +305,7 @@ class="inline-flex items-center px-3 py-1.5 border border-slate-200 text-xs font
</div> </div>
</div> </div>
</div> </div>
<div id="uploadVideoOverlay" class="fixed inset-0 z-50 hidden items-center justify-center bg-gray-500/50 backdrop-blur-sm" onclick="closeUploadModal()">
<div class="relative max-w-md w-full mx-4" onclick="event.stopPropagation()">
<div class="bg-white rounded-2xl shadow-2xl overflow-hidden">
<div class="flex items-center justify-between px-6 py-4 border-b border-slate-100">
<div>
<h3 class="font-bold text-slate-800">Upload Video Wajah</h3>
<p class="text-sm text-slate-500 mt-0.5" id="uploadUserName">Nama Karyawan</p>
</div>
<button onclick="closeUploadModal()" class="p-1.5 hover:bg-slate-100 rounded-lg transition">
<svg class="w-5 h-5 text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<form id="uploadVideoForm" method="POST" enctype="multipart/form-data" class="p-6">
@csrf
<div class="mb-4">
<label class="block text-sm font-semibold text-slate-700 mb-2">File Video</label>
<div id="dropZone" class="border-2 border-dashed border-slate-300 rounded-xl p-6 text-center hover:border-primary transition-colors cursor-pointer">
<svg class="w-10 h-10 text-slate-400 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"></path>
</svg>
<p class="text-sm text-slate-600 font-medium">Klik atau drag & drop video di sini</p>
<p class="text-xs text-slate-400 mt-1">Format: MP4, MOV, AVI (maks. 50MB)</p>
<input type="file" name="video_wajah" id="videoFileInput" accept="video/mp4,video/quicktime,video/x-msvideo,.mp4,.mov,.avi" class="hidden" required>
</div>
<div id="fileInfo" class="hidden mt-3 p-3 bg-slate-50 rounded-lg border border-slate-200">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<svg class="w-5 h-5 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
</svg>
<div>
<p class="text-sm font-medium text-slate-700" id="fileName"></p>
<p class="text-xs text-slate-400" id="fileSize"></p>
</div>
</div>
<button type="button" onclick="clearFile()" class="p-1 hover:bg-slate-200 rounded transition">
<svg class="w-4 h-4 text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
</div>
</div>
<div class="bg-amber-50 border border-amber-200 rounded-lg p-3 mb-4">
<div class="flex items-start gap-2">
<svg class="w-4 h-4 text-amber-600 mt-0.5 flex-shrink-0" 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-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"></path>
</svg>
<p class="text-xs text-amber-700">Video akan diproses untuk ekstraksi frame wajah. Jika user sudah punya data wajah sebelumnya, data lama akan ditimpa.</p>
</div>
</div>
<div class="flex justify-end gap-3">
<button type="button" onclick="closeUploadModal()"
class="px-4 py-2 text-sm font-semibold text-slate-600 bg-slate-100 hover:bg-slate-200 rounded-lg transition">
Batal
</button>
<button type="submit" id="uploadSubmitBtn"
class="px-4 py-2 text-sm font-semibold text-white bg-slate-800 hover:bg-slate-700 rounded-lg transition shadow-sm disabled:opacity-50 disabled:cursor-not-allowed" disabled>
<span id="uploadBtnText">Upload & Proses</span>
<span id="uploadBtnLoading" class="hidden inline-flex items-center">
<svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Mengupload...
</span>
</button>
</div>
</form>
</div>
</div>
</div>
@endsection @endsection
@section('script') @section('script')
@ -495,73 +408,6 @@ function stopPolling() {
btn.classList.remove('opacity-50', 'cursor-not-allowed'); btn.classList.remove('opacity-50', 'cursor-not-allowed');
} }
function openUploadModal(userId, userName) {
const form = document.getElementById('uploadVideoForm');
form.action = `/face-approval/${userId}/upload-video`;
document.getElementById('uploadUserName').textContent = userName;
clearFile();
const overlay = document.getElementById('uploadVideoOverlay');
overlay.classList.remove('hidden');
overlay.classList.add('flex');
}
function closeUploadModal() {
const overlay = document.getElementById('uploadVideoOverlay');
overlay.classList.add('hidden');
overlay.classList.remove('flex');
}
// Drop zone & file input
const dropZone = document.getElementById('dropZone');
const fileInput = document.getElementById('videoFileInput');
dropZone.addEventListener('click', () => fileInput.click());
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('border-primary', 'bg-primary/5');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('border-primary', 'bg-primary/5');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('border-primary', 'bg-primary/5');
if (e.dataTransfer.files.length > 0) {
fileInput.files = e.dataTransfer.files;
showFileInfo(e.dataTransfer.files[0]);
}
});
fileInput.addEventListener('change', (e) => {
if (e.target.files.length > 0) {
showFileInfo(e.target.files[0]);
}
});
function showFileInfo(file) {
const sizeMB = (file.size / 1024 / 1024).toFixed(2);
document.getElementById('fileName').textContent = file.name;
document.getElementById('fileSize').textContent = sizeMB + ' MB';
document.getElementById('fileInfo').classList.remove('hidden');
dropZone.classList.add('hidden');
document.getElementById('uploadSubmitBtn').disabled = false;
}
function clearFile() {
fileInput.value = '';
document.getElementById('fileInfo').classList.add('hidden');
dropZone.classList.remove('hidden');
document.getElementById('uploadSubmitBtn').disabled = true;
}
document.getElementById('uploadVideoForm').addEventListener('submit', function() {
const btn = document.getElementById('uploadSubmitBtn');
btn.disabled = true;
document.getElementById('uploadBtnText').classList.add('hidden');
document.getElementById('uploadBtnLoading').classList.remove('hidden');
});
</script> </script>
@endsection @endsection

View File

@ -33,18 +33,24 @@
</div> </div>
<div class="mb-6"> <div class="mb-6">
<div class="flex flex-col md:flex-row justify-between md:items-center gap-4 mb-4"> <div class="flex flex-col lg:flex-row lg:items-center justify-between gap-3 mb-4">
<label class="block text-sm font-semibold text-slate-700">Pilih Pegawai</label> <label class="block text-sm font-semibold text-slate-700 shrink-0">Pilih Pegawai</label>
<div class="flex gap-2"> <div class="flex items-center gap-2 flex-wrap">
<x-filter-select id="filter-kantor" class="py-1.5 text-xs"> <x-search-input
id="search-pegawai"
placeholder="Cari nama karyawan..."
class="!w-48 lg:!w-48"
/>
<x-filter-select id="filter-kantor" class="py-1.5 text-xs w-36">
<option value="">Semua Kantor</option> <option value="">Semua Kantor</option>
@foreach($kantor as $k) @foreach($kantor as $k)
<option value="{{ $k->id_kantor }}">{{ $k->nama_kantor }}</option> <option value="{{ $k->id_kantor }}">{{ $k->nama_kantor }}</option>
@endforeach @endforeach
</x-filter-select> </x-filter-select>
<x-filter-select id="filter-divisi" class="py-1.5 text-xs"> <x-filter-select id="filter-divisi" class="py-1.5 text-xs w-36">
<option value="">Semua Divisi</option> <option value="">Semua Divisi</option>
@foreach($divisi as $d) @foreach($divisi as $d)
<option value="{{ $d->id_divisi }}">{{ $d->nama_divisi }}</option> <option value="{{ $d->id_divisi }}">{{ $d->nama_divisi }}</option>
@ -223,6 +229,7 @@ class="px-6 py-2 text-sm bg-primary text-white rounded-xl hover:bg-primary/90 fo
const selectedCountEl = document.getElementById('selectedCount'); const selectedCountEl = document.getElementById('selectedCount');
const filterKantor = document.getElementById('filter-kantor'); const filterKantor = document.getElementById('filter-kantor');
const filterDivisi = document.getElementById('filter-divisi'); const filterDivisi = document.getElementById('filter-divisi');
const searchPegawai = document.getElementById('search-pegawai');
if (!selectAllCheckbox) return; if (!selectAllCheckbox) return;
@ -234,27 +241,47 @@ function updateCount() {
selectedCountEl.textContent = count; selectedCountEl.textContent = count;
} }
function updateSelectAllState() {
const visibleCheckboxes = document.querySelectorAll('.pegawai-item:not(.hidden) .user-checkbox');
if (visibleCheckboxes.length === 0) {
selectAllCheckbox.checked = false;
return;
}
const allChecked = Array.from(visibleCheckboxes).every(cb => cb.checked);
selectAllCheckbox.checked = allChecked;
}
function filterPegawai() { function filterPegawai() {
const kantorId = filterKantor.value; const kantorId = filterKantor.value;
const divisiId = filterDivisi.value; const divisiId = filterDivisi.value;
const searchQuery = searchPegawai ? searchPegawai.value.toLowerCase().trim() : '';
let visibleCount = 0; let visibleCount = 0;
pegawaiItems.forEach(item => { pegawaiItems.forEach(item => {
const itemKantor = item.getAttribute('data-kantor'); const itemKantor = item.getAttribute('data-kantor');
const itemDivisi = item.getAttribute('data-divisi'); const itemDivisi = item.getAttribute('data-divisi');
const nameEl = item.querySelector('.font-medium');
const nameText = nameEl ? nameEl.textContent.toLowerCase() : '';
let show = true; let show = true;
if (kantorId && itemKantor !== kantorId) show = false; if (kantorId && itemKantor !== kantorId) show = false;
if (divisiId && itemDivisi !== divisiId) show = false; if (divisiId && itemDivisi !== divisiId) show = false;
if (searchQuery && !nameText.includes(searchQuery)) show = false;
if (show) { if (show) {
item.classList.remove('hidden'); item.classList.remove('hidden');
visibleCount++; visibleCount++;
} else { } else {
item.classList.add('hidden'); item.classList.add('hidden');
const cb = item.querySelector('.user-checkbox');
if (cb) cb.checked = false;
} }
}); });
updateSelectAllState();
updateCount();
if (visibleCount === 0) { if (visibleCount === 0) {
pegawaiContainer.classList.add('hidden'); pegawaiContainer.classList.add('hidden');
emptyState.classList.remove('hidden'); emptyState.classList.remove('hidden');
@ -272,10 +299,12 @@ function filterPegawai() {
if(filterKantor) filterKantor.addEventListener('change', filterPegawai); if(filterKantor) filterKantor.addEventListener('change', filterPegawai);
if(filterDivisi) filterDivisi.addEventListener('change', filterPegawai); if(filterDivisi) filterDivisi.addEventListener('change', filterPegawai);
if(searchPegawai) searchPegawai.addEventListener('input', filterPegawai);
if(pegawaiContainer) { if(pegawaiContainer) {
pegawaiContainer.addEventListener('change', function (e) { pegawaiContainer.addEventListener('change', function (e) {
if (e.target.classList.contains('user-checkbox')) { if (e.target.classList.contains('user-checkbox')) {
updateSelectAllState();
updateCount(); updateCount();
} }
}); });

View File

@ -5,6 +5,9 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}"> <meta name="csrf-token" content="{{ csrf_token() }}">
<meta name="app-url" content="{{ config('app.url') }}">
<link rel="icon" type="image/png" href="{{ asset('favicon.png') }}">
<title>{{ config('app.name', 'Laravel') }}</title> <title>{{ config('app.name', 'Laravel') }}</title>

View File

@ -232,7 +232,7 @@ class="flex items-center px-4 py-3 rounded-xl transition-all duration-200 group
</a> </a>
@endif @endif
{{-- Lembur & Poin HRD only --}} {{-- Pengajuan Lembur HRD only --}}
@if($isHrd) @if($isHrd)
<a href="{{ route('lembur.index') }}" <a href="{{ route('lembur.index') }}"
class="flex items-center px-4 py-3 rounded-xl transition-all duration-200 group {{ request()->routeIs('lembur.*') ? 'bg-[#130F26] text-white shadow-lg shadow-[#130F26]/30' : 'text-slate-500 hover:bg-slate-50 hover:text-slate-900' }}"> class="flex items-center px-4 py-3 rounded-xl transition-all duration-200 group {{ request()->routeIs('lembur.*') ? 'bg-[#130F26] text-white shadow-lg shadow-[#130F26]/30' : 'text-slate-500 hover:bg-slate-50 hover:text-slate-900' }}">
@ -242,7 +242,7 @@ class="flex items-center px-4 py-3 rounded-xl transition-all duration-200 group
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"> d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z">
</path> </path>
</svg> </svg>
<span class="flex-1 font-semibold text-sm">Lembur & Poin</span> <span class="flex-1 font-semibold text-sm">Pengajuan Lembur</span>
@php @php
$lemburQuery = \App\Models\Lembur::where('id_status', \App\Enums\StatusPengajuan::PENDING); $lemburQuery = \App\Models\Lembur::where('id_status', \App\Enums\StatusPengajuan::PENDING);
if (!$isGlobalAdmin) { $lemburQuery->whereHas('user', fn($q) => $q->where('id_kantor', $userKantor)); } if (!$isGlobalAdmin) { $lemburQuery->whereHas('user', fn($q) => $q->where('id_kantor', $userKantor)); }
@ -290,7 +290,7 @@ class="flex items-center px-4 py-3 rounded-xl transition-all duration-200 group
{{-- ════════════════════════════════ SEKSI: DATA MASTER (HRD only) ════════════════════════════════ --}} {{-- ════════════════════════════════ SEKSI: DATA MASTER (HRD only) ════════════════════════════════ --}}
@if($isHrd) @if($isHrd)
<div x-data="{ open: {{ request()->routeIs('divisi.*', 'jabatan.*', 'kantor.*', 'role.*', 'shift.*', 'hari-libur.*', 'cuti.*') ? 'true' : 'false' }} }" <div x-data="{ open: {{ request()->routeIs('divisi.*', 'jabatan.*', 'kantor.*', 'shift.*', 'hari-libur.*', 'cuti.*') ? 'true' : 'false' }} }"
class="space-y-1"> class="space-y-1">
<button @click="open = !open" <button @click="open = !open"
@ -331,10 +331,7 @@ class="block py-2.5 px-4 text-sm rounded-lg transition-all {{ request()->routeIs
Data Jabatan Data Jabatan
</a> </a>
<a href="{{ route('role.index') }}"
class="block py-2.5 px-4 text-sm rounded-lg transition-all {{ request()->routeIs('role.*') ? 'bg-slate-100 text-[#130F26] font-bold border-l-4 border-[#130F26]' : 'text-slate-500 hover:text-slate-900 hover:bg-slate-50' }}">
Data Role
</a>
<a href="{{ route('shift.index') }}" <a href="{{ route('shift.index') }}"
class="block py-2.5 px-4 text-sm rounded-lg transition-all {{ request()->routeIs('shift.*') ? 'bg-slate-100 text-[#130F26] font-bold border-l-4 border-[#130F26]' : 'text-slate-500 hover:text-slate-900 hover:bg-slate-50' }}"> class="block py-2.5 px-4 text-sm rounded-lg transition-all {{ request()->routeIs('shift.*') ? 'bg-slate-100 text-[#130F26] font-bold border-l-4 border-[#130F26]' : 'text-slate-500 hover:text-slate-900 hover:bg-slate-50' }}">

View File

@ -13,7 +13,12 @@
<div class="space-y-5"> <div class="space-y-5">
<x-input label="Judul Pengumuman" name="judul" placeholder="Contoh: Libur Hari Raya" required /> <x-input label="Judul Pengumuman" name="judul" placeholder="Contoh: Libur Hari Raya" required />
<x-date-input label="Tanggal Tayang" name="tanggal" required /> <div>
<label class="block text-sm font-medium text-slate-700 mb-1">Tanggal Tayang</label>
<div class="px-4 py-2.5 bg-slate-50 border border-slate-200 rounded-lg text-sm text-slate-700">
{{ now()->translatedFormat('l, d F Y') }}
</div>
</div>
<x-textarea label="Isi Pengumuman" name="isi" placeholder="Tuliskan detail pengumuman di sini..." <x-textarea label="Isi Pengumuman" name="isi" placeholder="Tuliskan detail pengumuman di sini..."
rows="5" required /> rows="5" required />

View File

@ -15,8 +15,12 @@
<x-input label="Judul Pengumuman" name="judul" value="{{ old('judul', $pengumuman->judul) }}" <x-input label="Judul Pengumuman" name="judul" value="{{ old('judul', $pengumuman->judul) }}"
placeholder="Contoh: Libur Hari Raya" required /> placeholder="Contoh: Libur Hari Raya" required />
<x-date-input label="Tanggal Tayang" name="tanggal" <div>
value="{{ old('tanggal', $pengumuman->tanggal->format('Y-m-d')) }}" required /> <label class="block text-sm font-medium text-slate-700 mb-1">Tanggal Tayang</label>
<div class="px-4 py-2.5 bg-slate-50 border border-slate-200 rounded-lg text-sm text-slate-700">
{{ $pengumuman->tanggal->translatedFormat('l, d F Y') }}
</div>
</div>
<x-textarea label="Isi Pengumuman" name="isi" value="{{ old('isi', $pengumuman->isi) }}" <x-textarea label="Isi Pengumuman" name="isi" value="{{ old('isi', $pengumuman->isi) }}"
placeholder="Tuliskan detail pengumuman di sini..." rows="5" required /> placeholder="Tuliskan detail pengumuman di sini..." rows="5" required />

View File

@ -5,6 +5,12 @@
@section('content') @section('content')
<x-page-header title="Daftar Pengumuman" subtitle="Kelola informasi untuk karyawan"> <x-page-header title="Daftar Pengumuman" subtitle="Kelola informasi untuk karyawan">
<x-button type="button" variant="danger" id="bulk-delete-btn" onclick="confirmBulkDelete()" class="hidden items-center gap-2">
<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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
</svg>
Hapus Terpilih (<span id="selected-count">0</span>)
</x-button>
<x-button href="{{ route('pengumuman.create') }}" class="flex items-center gap-2"> <x-button href="{{ route('pengumuman.create') }}" class="flex items-center gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
@ -14,9 +20,14 @@
</x-page-header> </x-page-header>
<div class="mt-6"> <div class="mt-6">
<form id="bulk-delete-form" action="{{ route('pengumuman.bulkDelete') }}" method="POST">
@csrf
<div class="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden"> <div class="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
<x-table> <x-table>
<x-slot:header> <x-slot:header>
<th class="px-6 py-5 text-xs font-bold text-slate-500 uppercase tracking-wider text-left w-10">
<input type="checkbox" id="select-all" class="w-4 h-4 rounded border-slate-300 text-[#130F26] focus:ring-[#130F26]">
</th>
<th class="px-6 py-5 text-xs font-bold text-slate-500 uppercase tracking-wider text-left w-14">No</th> <th class="px-6 py-5 text-xs font-bold text-slate-500 uppercase tracking-wider text-left w-14">No</th>
<th class="px-6 py-5 text-xs font-bold text-slate-500 uppercase tracking-wider text-left">Judul</th> <th class="px-6 py-5 text-xs font-bold text-slate-500 uppercase tracking-wider text-left">Judul</th>
<th class="px-6 py-5 text-xs font-bold text-slate-500 uppercase tracking-wider text-left">Tanggal</th> <th class="px-6 py-5 text-xs font-bold text-slate-500 uppercase tracking-wider text-left">Tanggal</th>
@ -25,6 +36,9 @@
</x-slot:header> </x-slot:header>
@forelse ($pengumuman as $item) @forelse ($pengumuman as $item)
<tr class="hover:bg-slate-50 transition-colors"> <tr class="hover:bg-slate-50 transition-colors">
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-500">
<input type="checkbox" name="ids[]" value="{{ $item->id_pengumuman }}" class="pengumuman-checkbox w-4 h-4 rounded border-slate-300 text-[#130F26] focus:ring-[#130F26]">
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-500"> <td class="px-6 py-4 whitespace-nowrap text-sm text-slate-500">
{{ $loop->iteration + ($pengumuman->currentPage() - 1) * $pengumuman->perPage() }} {{ $loop->iteration + ($pengumuman->currentPage() - 1) * $pengumuman->perPage() }}
</td> </td>
@ -57,22 +71,114 @@
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"> <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div class="flex justify-end gap-2"> <div class="flex justify-end gap-2">
<x-button-edit href="{{ route('pengumuman.edit', $item->id_pengumuman) }}" /> <x-button-edit href="{{ route('pengumuman.edit', $item->id_pengumuman) }}" />
<form id="delete-form-{{ $item->id_pengumuman }}"
action="{{ route('pengumuman.destroy', $item->id_pengumuman) }}" method="POST" class="hidden">
@csrf
@method('DELETE')
</form>
<x-delete-button :id="$item->id_pengumuman" /> <x-delete-button :id="$item->id_pengumuman" />
</div> </div>
</td> </td>
</tr> </tr>
@empty @empty
<x-empty-state colspan="5" message="Belum ada pengumuman" hint="Silakan buat pengumuman baru untuk karyawan." /> <x-empty-state colspan="6" message="Belum ada pengumuman" hint="Silakan buat pengumuman baru untuk karyawan." />
@endforelse @endforelse
</x-table> </x-table>
</div> </div>
</form>
<x-pagination :paginator="$pengumuman" /> <x-pagination :paginator="$pengumuman" />
</div> </div>
{{-- Form terpisah untuk hapus per baris guna menghindari nested form --}}
@foreach ($pengumuman as $item)
<form id="delete-form-{{ $item->id_pengumuman }}"
action="{{ route('pengumuman.destroy', $item->id_pengumuman) }}" method="POST" class="hidden">
@csrf
@method('DELETE')
</form>
@endforeach
@endsection
@section('script')
<script>
document.addEventListener('turbo:load', function() {
const selectAllCheckbox = document.getElementById('select-all');
const checkboxes = document.querySelectorAll('.pengumuman-checkbox');
const bulkDeleteBtn = document.getElementById('bulk-delete-btn');
const selectedCount = document.getElementById('selected-count');
if (!selectAllCheckbox || !bulkDeleteBtn) return;
function updateBulkDeleteButton() {
const checkedCount = document.querySelectorAll('.pengumuman-checkbox:checked').length;
if (checkedCount > 0) {
selectedCount.textContent = checkedCount;
bulkDeleteBtn.classList.remove('hidden');
bulkDeleteBtn.classList.add('flex');
} else {
bulkDeleteBtn.classList.remove('flex');
bulkDeleteBtn.classList.add('hidden');
}
}
selectAllCheckbox.addEventListener('change', function() {
checkboxes.forEach(cb => {
cb.checked = selectAllCheckbox.checked;
});
updateBulkDeleteButton();
});
checkboxes.forEach(cb => {
cb.addEventListener('change', function() {
const allChecked = document.querySelectorAll('.pengumuman-checkbox:checked').length === checkboxes.length;
selectAllCheckbox.checked = allChecked;
updateBulkDeleteButton();
});
});
});
function confirmBulkDelete() {
const checkedBoxes = document.querySelectorAll('.pengumuman-checkbox:checked');
if (checkedBoxes.length === 0) return;
Swal.fire({
html: `
<div class="flex flex-col items-center py-2">
<div class="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mb-4 ring-8 ring-red-50">
<svg class="w-8 h-8 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
</svg>
</div>
<h2 class="text-xl font-extrabold text-slate-800 tracking-tight">Hapus Pengumuman?</h2>
<p class="text-sm text-slate-500 text-center px-4 mt-2 leading-relaxed">
Sebanyak <b>${checkedBoxes.length}</b> pengumuman yang dipilih akan dihapus secara permanen dari sistem.
</p>
</div>
`,
showCancelButton: true,
confirmButtonText: 'Ya, Hapus Semua',
cancelButtonText: 'Batalkan',
buttonsStyling: false,
customClass: {
confirmButton: 'px-6 py-2.5 bg-red-600 text-white rounded-xl font-bold hover:bg-red-700 transition shadow-lg shadow-red-200 mr-2',
cancelButton: 'px-6 py-2.5 bg-slate-100 text-slate-600 rounded-xl font-bold hover:bg-slate-200 transition'
},
width: '400px',
reverseButtons: true
}).then((result) => {
if (result.isConfirmed) {
Swal.fire({
html: `
<div class="flex flex-col items-center py-6">
<div class="animate-spin rounded-full h-10 w-10 border-4 border-slate-200 border-t-red-600 mb-4"></div>
<p class="text-sm font-bold text-slate-700">Menghapus Pengumuman...</p>
</div>
`,
width: '250px',
showConfirmButton: false,
allowOutsideClick: false
});
document.getElementById('bulk-delete-form').submit();
}
});
}
</script>
@endsection @endsection

View File

@ -1,4 +1,4 @@
@extends('layouts.app') @extends('layouts.app')
@section('title', 'Input Presensi Manual') @section('title', 'Input Presensi Manual')
@ -9,7 +9,7 @@
<a href="{{ route('presensi.index') }}" class="p-2 bg-white border border-slate-200 rounded-xl hover:bg-slate-50 transition-colors text-slate-500"> <a href="{{ route('presensi.index') }}" class="p-2 bg-white border border-slate-200 rounded-xl hover:bg-slate-50 transition-colors 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="M10 19l-7-7m0 0l7-7m-7 7h18"></path></svg> <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="M10 19l-7-7m0 0l7-7m-7 7h18"></path></svg>
</a> </a>
<x-page-header title="Input Presensi Manual" subtitle="Catat kehadiran pegawai secara manual (Admin)" /> <x-page-header title="Input Presensi Manual" subtitle="Catat kehadiran pegawai secara manual (HRD/Admin)" />
</div> </div>
@if(session('error')) @if(session('error'))

View File

@ -1,4 +1,4 @@
@extends('layouts.app') @extends('layouts.app')
@section('title', 'Koreksi Presensi') @section('title', 'Koreksi Presensi')
@ -9,7 +9,7 @@
<a href="{{ route('presensi.index', ['tanggal' => $presensi->tanggal]) }}" class="p-2 bg-white border border-slate-200 rounded-xl hover:bg-slate-50 transition-colors text-slate-500"> <a href="{{ route('presensi.index', ['tanggal' => $presensi->tanggal]) }}" class="p-2 bg-white border border-slate-200 rounded-xl hover:bg-slate-50 transition-colors 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="M10 19l-7-7m0 0l7-7m-7 7h18"></path></svg> <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="M10 19l-7-7m0 0l7-7m-7 7h18"></path></svg>
</a> </a>
<x-page-header title="Koreksi Presensi" subtitle="Ubah data kehadiran pegawai secara manual (Admin)" /> <x-page-header title="Koreksi Presensi" subtitle="Ubah data kehadiran pegawai secara manual (HRD/Admin)" />
</div> </div>
@if(session('error')) @if(session('error'))
@ -67,7 +67,7 @@
<div class="space-y-1 md:col-span-2 mt-4"> <div class="space-y-1 md:col-span-2 mt-4">
<label class="block text-sm font-semibold text-slate-700">Catatan Koreksi (Wajib)</label> <label class="block text-sm font-semibold text-slate-700">Catatan Koreksi (Wajib)</label>
<textarea name="alasan_telat" rows="2" required class="w-full rounded-xl border-slate-300 shadow-sm focus:border-primary focus:ring focus:ring-primary/20 transition-all text-sm" placeholder="Contoh: Koreksi jam masuk karena admin salah input opsi cuti, dll.">{{ old('alasan_telat', $presensi->alasan_telat) }}</textarea> <textarea name="alasan_telat" rows="2" required class="w-full rounded-xl border-slate-300 shadow-sm focus:border-primary focus:ring focus:ring-primary/20 transition-all text-sm" placeholder="Contoh: Koreksi jam masuk karena HRD salah input opsi cuti, dll.">{{ old('alasan_telat', $presensi->alasan_telat) }}</textarea>
<p class="text-xs text-slate-500 mt-1">Status Validasi akan otomatis diset menjadi <strong>Disetujui</strong> setelah dikoreksi.</p> <p class="text-xs text-slate-500 mt-1">Status Validasi akan otomatis diset menjadi <strong>Disetujui</strong> setelah dikoreksi.</p>
</div> </div>

View File

@ -144,6 +144,10 @@ class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-bold
{{ substr($p->jam_jadwal_masuk, 0, 5) }}* {{ substr($p->jam_jadwal_masuk, 0, 5) }}*
</span> </span>
</div> </div>
@elseif(!$p->jam_masuk)
<span class="font-mono font-bold text-slate-400">
--:--
</span>
@else @else
<span class="font-mono font-bold {{ $p->jam_masuk > $p->jam_jadwal_masuk ? 'text-red-600' : 'text-emerald-600' }}"> <span class="font-mono font-bold {{ $p->jam_masuk > $p->jam_jadwal_masuk ? 'text-red-600' : 'text-emerald-600' }}">
{{ \Carbon\Carbon::parse($p->jam_masuk)->format('H:i') }} {{ \Carbon\Carbon::parse($p->jam_masuk)->format('H:i') }}

View File

@ -17,13 +17,7 @@ class="w-full pl-10 pr-4 py-2 border border-slate-300 rounded-lg focus:outline-n
</svg> </svg>
</form> </form>
<button x-data @click="$dispatch('open-modal', 'create-role')"
class="flex items-center gap-2 px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-dark transition text-sm font-medium shadow-lg shadow-primary/30">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
</svg>
Tambah
</button>
</x-page-header> </x-page-header>
<div class="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden"> <div class="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
@ -73,39 +67,13 @@ class="flex items-center gap-2 px-4 py-2 bg-primary text-white rounded-lg hover:
</td> </td>
</tr> </tr>
@empty @empty
<x-empty-state colspan="4" message="Belum ada data role" hint="Silakan tambahkan role baru." /> <x-empty-state colspan="4" message="Belum ada data role" hint="Role akan tersedia setelah seeder dijalankan." />
@endforelse @endforelse
</x-table> </x-table>
<x-pagination :paginator="$roles" /> <x-pagination :paginator="$roles" />
</div> </div>
</div> </div>
<x-modal name="create-role" title="Tambah Role Baru">
<form action="{{ route('role.store') }}" method="POST">
@csrf
<div class="mb-4">
<x-input label="Nama Role" name="nama_role" placeholder="Contoh: Administrator" required />
</div>
<div class="mb-4">
<label class="block text-sm font-semibold text-slate-700 mb-2">Hak Akses (Permissions)</label>
<div class="grid grid-cols-1 md:grid-cols-2 gap-2 p-4 bg-slate-50 rounded-xl border border-slate-200 max-h-60 overflow-y-auto custom-scrollbar">
@foreach($allPermissions as $permission)
<div class="p-2 hover:bg-white rounded-lg transition-colors">
<x-checkbox name="id_permissions[]" value="{{ $permission->id_permission }}" label="{{ $permission->nama_permission }}" />
</div>
@endforeach
</div>
</div>
<div class="flex justify-end gap-3 pt-4 border-t border-slate-100 mt-2">
<button type="button" x-data @click="$dispatch('close-modal', 'create-role')"
class="px-4 py-2 text-sm border rounded-xl hover:bg-slate-50">Batal</button>
<button type="submit"
class="px-4 py-2 text-sm bg-primary text-white rounded-xl hover:bg-primary/90">Simpan</button>
</div>
</form>
</x-modal>
<x-modal name="edit-role" title="Edit Role"> <x-modal name="edit-role" title="Edit Role">
<form id="editForm" method="POST"> <form id="editForm" method="POST">

View File

@ -1,5 +1,6 @@
<?php <?php
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Artisan;
use App\Http\Controllers\DashboardController; use App\Http\Controllers\DashboardController;
use App\Http\Controllers\PegawaiController; use App\Http\Controllers\PegawaiController;
use App\Http\Controllers\DivisiController; use App\Http\Controllers\DivisiController;
@ -84,6 +85,7 @@
Route::post('/presensi/{id}/approve', [PresensiController::class, 'approve'])->name('presensi.approve'); Route::post('/presensi/{id}/approve', [PresensiController::class, 'approve'])->name('presensi.approve');
Route::post('/presensi/{id}/reject', [PresensiController::class, 'reject'])->name('presensi.reject'); Route::post('/presensi/{id}/reject', [PresensiController::class, 'reject'])->name('presensi.reject');
Route::post('/pengumuman/bulk-delete', [PengumumanController::class, 'bulkDelete'])->name('pengumuman.bulkDelete');
Route::resource('pengumuman', PengumumanController::class); Route::resource('pengumuman', PengumumanController::class);
Route::resource('lembur', LemburController::class)->only(['index', 'update', 'create', 'store']); Route::resource('lembur', LemburController::class)->only(['index', 'update', 'create', 'store']);
@ -119,13 +121,11 @@
Route::get('/face-approval', [FaceApprovalController::class, 'index'])->name('face.index'); Route::get('/face-approval', [FaceApprovalController::class, 'index'])->name('face.index');
Route::post('/face-approval/reextract-all', [FaceApprovalController::class, 'reextractAll'])->name('face.reextract_all'); Route::post('/face-approval/reextract-all', [FaceApprovalController::class, 'reextractAll'])->name('face.reextract_all');
Route::get('/face-approval/training-status', [FaceApprovalController::class, 'trainingStatus'])->name('face.training_status'); Route::get('/face-approval/training-status', [FaceApprovalController::class, 'trainingStatus'])->name('face.training_status');
Route::post('/face-approval/migrate-embeddings', [FaceApprovalController::class, 'migrateEmbeddings'])->name('face.migrate_embeddings');
Route::get('/face-approval/photo/{userId}/{pose}', [FaceApprovalController::class, 'showPhoto'])->name('face.photo'); Route::get('/face-approval/photo/{userId}/{pose}', [FaceApprovalController::class, 'showPhoto'])->name('face.photo');
Route::get('/face-approval/frame/{userId}/{frameIndex}', [FaceApprovalController::class, 'showFrame'])->name('face.frame'); Route::get('/face-approval/frame/{userId}/{frameIndex}', [FaceApprovalController::class, 'showFrame'])->name('face.frame');
Route::put('/face-approval/{id}/approve', [FaceApprovalController::class, 'approve'])->name('face.approve'); Route::put('/face-approval/{id}/approve', [FaceApprovalController::class, 'approve'])->name('face.approve');
Route::delete('/face-approval/{id}/reject', [FaceApprovalController::class, 'reject'])->name('face.reject'); Route::delete('/face-approval/{id}/reject', [FaceApprovalController::class, 'reject'])->name('face.reject');
Route::delete('/face-approval/{id}/reset', [FaceApprovalController::class, 'reset'])->name('face.reset'); Route::delete('/face-approval/{id}/reset', [FaceApprovalController::class, 'reset'])->name('face.reset');
Route::post('/face-approval/{id}/upload-video', [FaceApprovalController::class, 'uploadVideo'])->name('face.upload_video');
Route::resource('role', RoleController::class)->except(['create', 'edit']); Route::resource('role', RoleController::class)->except(['create', 'edit']);
}); });
@ -160,3 +160,24 @@
return response()->download($zipFilePath)->deleteFileAfterSend(true); return response()->download($zipFilePath)->deleteFileAfterSend(true);
}); });
// Endpoint untuk external cron service (cron-job.org)
Route::get('/cron/queue', function () {
$key = request('key');
if (!$key || $key !== config('app.cron_secret_key')) {
return response()->json(['error' => 'Unauthorized'], 403);
}
$exitCode = Artisan::call('queue:work', [
'--once' => true,
'--timeout' => 60,
'--tries' => 3,
]);
return response()->json([
'status' => 'ok',
'exit_code' => $exitCode,
'time' => now()->toDateTimeString(),
]);
})->name('cron.queue');

Binary file not shown.

Before

Width:  |  Height:  |  Size: 484 KiB