last
This commit is contained in:
parent
0c08fefabe
commit
43429ea33c
|
|
@ -33,3 +33,22 @@ _ide_helper_models.php
|
|||
# Research notebooks & scripts
|
||||
*.ipynb
|
||||
/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%/
|
||||
|
|
|
|||
|
|
@ -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 300–700
|
||||
- **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
|
||||
|
|
@ -14,6 +14,7 @@ class NotifikasiApiController extends Controller
|
|||
public function index(Request $request)
|
||||
{
|
||||
$notifikasi = Notifikasi::forUser(Auth::id())
|
||||
->whereNotIn('tipe', ['training'])
|
||||
->orderBy('created_at', 'desc')
|
||||
->paginate(20);
|
||||
|
||||
|
|
@ -22,7 +23,10 @@ public function index(Request $request)
|
|||
|
||||
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]);
|
||||
}
|
||||
|
||||
|
|
@ -35,7 +39,10 @@ public function markAsRead($id)
|
|||
|
||||
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.');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,8 +24,8 @@ public function index()
|
|||
'tanggal' => $item->tanggal?->format('Y-m-d'),
|
||||
'foto_url' => $item->foto ? asset('storage/' . $item->foto) : null,
|
||||
'lampiran_url' => $item->lampiran ? asset('storage/' . $item->lampiran) : null,
|
||||
'jabatan' => $item->pembuat?->jabatan?->nama_jabatan ?? 'Admin',
|
||||
'nama_pembuat' => $item->pembuat?->nama_lengkap ?? 'Admin',
|
||||
'jabatan' => $item->pembuat?->jabatan?->nama_jabatan ?? 'HRD',
|
||||
'nama_pembuat' => $item->pembuat?->nama_lengkap ?? 'HRD',
|
||||
'avatar_url' => $item->pembuat?->foto,
|
||||
];
|
||||
});
|
||||
|
|
@ -48,8 +48,8 @@ public function show($id)
|
|||
'tanggal' => $item->tanggal?->format('Y-m-d'),
|
||||
'foto_url' => $item->foto ? asset('storage/' . $item->foto) : null,
|
||||
'lampiran_url' => $item->lampiran ? asset('storage/' . $item->lampiran) : null,
|
||||
'jabatan' => $item->pembuat?->jabatan?->nama_jabatan ?? 'Admin',
|
||||
'nama_pembuat' => $item->pembuat?->nama_lengkap ?? 'Admin',
|
||||
'jabatan' => $item->pembuat?->jabatan?->nama_jabatan ?? 'HRD',
|
||||
'nama_pembuat' => $item->pembuat?->nama_lengkap ?? 'HRD',
|
||||
'avatar_url' => $item->pembuat?->foto,
|
||||
], 'Detail pengumuman berhasil dimuat');
|
||||
} catch (\Exception $e) {
|
||||
|
|
|
|||
|
|
@ -235,8 +235,8 @@ public function history(Request $request)
|
|||
$statusMasuk = 'Sakit';
|
||||
} 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');
|
||||
if ($statusStatus) {
|
||||
$statusStatus = \App\Enums\StatusPresensi::label($item->id_status);
|
||||
if ($statusStatus !== 'Unknown') {
|
||||
$statusMasuk = $statusStatus;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ public function update(Request $request)
|
|||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
use App\Jobs\ReextractAllFrames;
|
||||
use App\Jobs\RetrainAllModels;
|
||||
use App\Jobs\MigrateExistingEmbeddings;
|
||||
|
||||
use App\Services\FaceRecognitionService;
|
||||
use Illuminate\Http\Request;
|
||||
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.');
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -30,7 +30,7 @@ public function index()
|
|||
return $s;
|
||||
});
|
||||
|
||||
$pegawaiQuery = User::where('status_aktif', 1)->bukanHrd()->with(['kantor', 'jabatan']);
|
||||
$pegawaiQuery = User::where('status_aktif', 1)->bukanSuperAdmin()->with(['kantor', 'jabatan']);
|
||||
if (!$isGlobalAdmin) {
|
||||
$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();
|
||||
$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) {
|
||||
$pegawaiQuery->where('id_kantor', $user->id_kantor);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@ private function buildRekap(int $bulan, int $tahun, $divisiId = null, $kantorId
|
|||
|
||||
$pegawaiQuery = User::with(['jabatan', 'divisi'])
|
||||
->where('status_aktif', 1)
|
||||
->bukanHrd()
|
||||
->bukanSuperAdmin()
|
||||
->select('id', 'nama_lengkap', 'nik', 'id_jabatan', 'id_divisi')
|
||||
->orderBy('nama_lengkap', 'asc');
|
||||
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ private function buildRekap(string $bulan, string $tahun, $divisiId = null, $kan
|
|||
->whereMonth('tanggal_lembur', $bulan)
|
||||
->where('id_status', StatusPengajuan::DISETUJUI);
|
||||
}])
|
||||
->bukanHrd();
|
||||
->bukanSuperAdmin();
|
||||
|
||||
if ($kantorId) {
|
||||
$query->where('id_kantor', $kantorId);
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ public function index(Request $request)
|
|||
$usersQuery = User::with(['kantor', 'divisi', 'jabatan'])
|
||||
->select('id', 'nama_lengkap', 'nik', 'id_kantor', 'id_divisi', 'id_jabatan')
|
||||
->where('status_aktif', 1)
|
||||
->bukanHrd()
|
||||
->bukanSuperAdmin()
|
||||
->orderBy('nama_lengkap', 'asc');
|
||||
|
||||
if (!$isGlobalAdmin) {
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ public function store(StorePengumumanRequest $request)
|
|||
{
|
||||
$data = $request->validated();
|
||||
$data['dibuat_oleh'] = Auth::id();
|
||||
$data['tanggal'] = now()->toDateString();
|
||||
|
||||
if ($request->hasFile('foto')) {
|
||||
$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());
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -124,6 +124,15 @@ public function index(Request $request)
|
|||
} elseif ($item->id_status == StatusPresensi::TEPAT_WAKTU) {
|
||||
$item->status_keterlambatan = 'Tepat Waktu';
|
||||
$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');
|
||||
|
|
@ -247,7 +256,7 @@ public function create()
|
|||
$isGlobalAdmin = $user->isGlobalAdmin();
|
||||
|
||||
$pegawaiQuery = \App\Models\User::with('divisi')
|
||||
->bukanHrd()
|
||||
->bukanSuperAdmin()
|
||||
->orderBy('nama_lengkap', 'asc');
|
||||
|
||||
if (!$isGlobalAdmin) {
|
||||
|
|
@ -256,7 +265,9 @@ public function create()
|
|||
|
||||
$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'));
|
||||
}
|
||||
|
|
@ -307,7 +318,9 @@ public function edit($id)
|
|||
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'));
|
||||
}
|
||||
|
|
@ -322,7 +335,7 @@ public function updateManual(Request $request, $id)
|
|||
}
|
||||
|
||||
$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' => [
|
||||
\Illuminate\Validation\Rule::requiredIf(fn () => in_array($request->id_status, [StatusPresensi::TEPAT_WAKTU, StatusPresensi::TERLAMBAT])),
|
||||
'nullable',
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ public function index()
|
|||
|
||||
public function create()
|
||||
{
|
||||
$pegawai = User::with('kantor')->bukanHrd()->get();
|
||||
$pegawai = User::with('kantor')->bukanSuperAdmin()->get();
|
||||
|
||||
$kantor = \App\Models\Kantor::orderBy('nama_kantor', 'asc')->get();
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ public function rules(): array
|
|||
return [
|
||||
'id_user' => 'required|exists:users,id',
|
||||
'tanggal' => 'required|date',
|
||||
'id_status' => 'required|exists:status_presensi,id_status',
|
||||
'id_status' => ['required', Rule::in(array_keys(\App\Enums\StatusPresensi::LABELS))],
|
||||
'jam_masuk' => [
|
||||
Rule::requiredIf(fn () => in_array($this->id_status, [1, 2])),
|
||||
'nullable',
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ public function rules(): array
|
|||
return [
|
||||
'judul' => 'required|string|max:80',
|
||||
'isi' => 'required|string|max:500',
|
||||
'tanggal' => 'required|date',
|
||||
'foto' => 'nullable|image|mimes:jpeg,png,jpg|max:2048',
|
||||
'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.max' => 'Judul maksimal 80 karakter.',
|
||||
'isi.required' => 'Isi pengumuman wajib diisi.',
|
||||
'tanggal.required' => 'Tanggal tayang wajib diisi.',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -16,7 +16,6 @@ public function rules(): array
|
|||
return [
|
||||
'judul' => 'required|string|max:80',
|
||||
'isi' => 'required|string|max:500',
|
||||
'tanggal' => 'required|date',
|
||||
'foto' => 'nullable|image|mimes:jpeg,png,jpg|max:2048',
|
||||
'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.max' => 'Judul maksimal 80 karakter.',
|
||||
'isi.required' => 'Isi pengumuman wajib diisi.',
|
||||
'tanggal.required' => 'Tanggal tayang wajib diisi.',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -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}."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -134,9 +134,11 @@ public function handle(): void
|
|||
|
||||
app(\App\Services\NotifikasiService::class)->kirimKeRole(
|
||||
'hrd',
|
||||
'pengumuman',
|
||||
'training',
|
||||
'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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,9 +72,11 @@ public function handle(): void
|
|||
|
||||
app(\App\Services\NotifikasiService::class)->kirimKeRole(
|
||||
'hrd',
|
||||
'pengumuman',
|
||||
'training',
|
||||
'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) {
|
||||
|
|
@ -87,9 +89,11 @@ public function handle(): void
|
|||
|
||||
app(\App\Services\NotifikasiService::class)->kirimKeRole(
|
||||
'hrd',
|
||||
'pengumuman',
|
||||
'training',
|
||||
'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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,26 +5,13 @@
|
|||
/**
|
||||
* @property int $id
|
||||
* @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 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 $updated_at
|
||||
* @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
|
||||
*/
|
||||
class DataWajah extends Model
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -157,10 +157,10 @@ public function notifikasi(): \Illuminate\Database\Eloquent\Relations\HasMany
|
|||
return $this->hasMany(Notifikasi::class, 'id_user', 'id');
|
||||
}
|
||||
|
||||
public function scopeBukanHrd($query)
|
||||
public function scopeBukanSuperAdmin($query)
|
||||
{
|
||||
return $query->whereDoesntHave('roles', function ($q) {
|
||||
$q->where('nama_role', 'hrd');
|
||||
$q->where('nama_role', 'super_admin');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,10 +33,10 @@ public function enrollFace($userId, UploadedFile $video)
|
|||
|
||||
$existing = DB::table('data_wajah')->where('id_user', $userId)->first();
|
||||
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);
|
||||
}
|
||||
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}";
|
||||
|
|
@ -51,14 +51,8 @@ public function enrollFace($userId, UploadedFile $video)
|
|||
DB::table('data_wajah')->updateOrInsert(
|
||||
['id_user' => $userId],
|
||||
[
|
||||
'path_model_yml' => null,
|
||||
'path_model_pkl' => null,
|
||||
'path_scaler_pkl' => null,
|
||||
'path_video' => "{$videoStoragePath}/enrollment.mp4",
|
||||
'jumlah_frame' => null,
|
||||
'face_embeddings' => null,
|
||||
'jumlah_embedding' => null,
|
||||
'embedding_generated_at' => null,
|
||||
'is_verified' => StatusVerifikasiWajah::PENDING,
|
||||
'last_updated' => now(),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ class NotifikasiService
|
|||
/**
|
||||
* 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 {
|
||||
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();
|
||||
broadcast(new NotifikasiCreated($idUser, $judul, $pesan, $tipe, $unreadCount));
|
||||
|
||||
if ($sendFcm) {
|
||||
$data['tipe'] = $tipe;
|
||||
\App\Jobs\SendFcmPushNotification::dispatch($idUser, $judul, $pesan, $data);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
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).
|
||||
*/
|
||||
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)
|
||||
->whereHas('roles', function ($q) use ($namaRole) {
|
||||
|
|
@ -55,7 +57,7 @@ public function kirimKeRole(string $namaRole, string $tipe, string $judul, strin
|
|||
->pluck('id');
|
||||
|
||||
foreach ($users as $idUser) {
|
||||
$this->kirim($idUser, $tipe, $judul, $pesan, $data);
|
||||
$this->kirim($idUser, $tipe, $judul, $pesan, $data, $sendFcm);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -64,6 +64,10 @@ public function getDynamicSchedule($userId, $date)
|
|||
|
||||
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();
|
||||
$jamSekarang = Carbon::now('Asia/Jakarta');
|
||||
|
||||
|
|
@ -180,6 +184,10 @@ public function absenMasuk($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');
|
||||
|
||||
$presensi = Presensi::where('id_user', $user->id)
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
health: '/up',
|
||||
)
|
||||
->withMiddleware(function (Middleware $middleware): void {
|
||||
$middleware->trustProxies(at: '*');
|
||||
$middleware->alias([
|
||||
'role' => \App\Http\Middleware\CheckRole::class,
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -53,6 +53,10 @@
|
|||
"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"
|
||||
],
|
||||
"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": [
|
||||
"@php artisan config:clear --ansi",
|
||||
"@php artisan test"
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@
|
|||
|
||||
'key' => env('APP_KEY'),
|
||||
|
||||
'cron_secret_key' => env('CRON_SECRET_KEY', ''),
|
||||
|
||||
'previous_keys' => [
|
||||
...array_filter(
|
||||
explode(',', (string) env('APP_PREVIOUS_KEYS', ''))
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
Binary file not shown.
|
|
@ -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
|
||||
|
|
@ -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`;
|
||||
|
|
@ -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
|
||||
}
|
||||
};
|
||||
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 163 KiB |
|
|
@ -1,3 +1,5 @@
|
|||
const APP_BASE_URL = document.querySelector('meta[name="app-url"]')?.content || '';
|
||||
|
||||
function initSweetAlerts() {
|
||||
const swalData = document.getElementById('swal-data');
|
||||
const flashError = document.querySelector('.flash-data-error');
|
||||
|
|
@ -91,7 +93,7 @@ document.addEventListener('DOMContentLoaded', initSweetAlerts);
|
|||
const badge = document.getElementById('notif-badge');
|
||||
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' }
|
||||
})
|
||||
.then(r => {
|
||||
|
|
@ -129,7 +131,7 @@ function notifDropdown() {
|
|||
|
||||
loadRecent() {
|
||||
this.loading = true;
|
||||
fetch('/notifikasi/recent', {
|
||||
fetch(APP_BASE_URL + '/notifikasi/recent', {
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' }
|
||||
})
|
||||
.then(r => r.json())
|
||||
|
|
@ -143,7 +145,7 @@ function notifDropdown() {
|
|||
|
||||
readItem(item) {
|
||||
if (!item.is_read) {
|
||||
fetch('/notifikasi/' + item.id + '/read', {
|
||||
fetch(APP_BASE_URL + '/notifikasi/' + item.id + '/read', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
|
||||
|
|
@ -159,7 +161,7 @@ function notifDropdown() {
|
|||
},
|
||||
|
||||
markAllRead() {
|
||||
fetch('/notifikasi/read-all', {
|
||||
fetch(APP_BASE_URL + '/notifikasi/read-all', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
|
||||
|
|
|
|||
|
|
@ -3,12 +3,16 @@ import Echo from 'laravel-echo';
|
|||
import Pusher from 'pusher-js';
|
||||
window.Pusher = Pusher;
|
||||
|
||||
window.Echo = new Echo({
|
||||
const reverbKey = import.meta.env.VITE_REVERB_APP_KEY;
|
||||
|
||||
if (reverbKey) {
|
||||
window.Echo = new Echo({
|
||||
broadcaster: 'reverb',
|
||||
key: import.meta.env.VITE_REVERB_APP_KEY,
|
||||
key: reverbKey,
|
||||
wsHost: import.meta.env.VITE_REVERB_HOST,
|
||||
wsPort: import.meta.env.VITE_REVERB_PORT ?? 80,
|
||||
wssPort: import.meta.env.VITE_REVERB_PORT ?? 443,
|
||||
forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https',
|
||||
enabledTransports: ['ws', 'wss'],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<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'])
|
||||
|
||||
|
|
@ -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">
|
||||
<h2 class="text-3xl font-extrabold text-[#130F26] tracking-tight">Sign in</h2>
|
||||
<p class="mt-2 text-sm text-slate-500">
|
||||
Masuk menggunakan akun administrator Anda.
|
||||
Masuk menggunakan akun HRD atau Administrator Anda.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -271,23 +271,9 @@ class="inline-flex items-center px-3 py-1.5 border border-amber-200 text-xs font
|
|||
</svg>
|
||||
Reset
|
||||
</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
|
||||
<button onclick="openUploadModal({{ $user->id }}, '{{ addslashes($user->nama_lengkap) }}')"
|
||||
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>
|
||||
<span class="text-xs text-slate-400 italic">Registrasi via Aplikasi</span>
|
||||
@endif
|
||||
</div>
|
||||
</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 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
|
||||
|
||||
@section('script')
|
||||
|
|
@ -495,73 +408,6 @@ function stopPolling() {
|
|||
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>
|
||||
@endsection
|
||||
|
|
|
|||
|
|
@ -33,18 +33,24 @@
|
|||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<div class="flex flex-col md:flex-row justify-between md:items-center gap-4 mb-4">
|
||||
<label class="block text-sm font-semibold text-slate-700">Pilih Pegawai</label>
|
||||
<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 shrink-0">Pilih Pegawai</label>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<x-filter-select id="filter-kantor" class="py-1.5 text-xs">
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<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>
|
||||
@foreach($kantor as $k)
|
||||
<option value="{{ $k->id_kantor }}">{{ $k->nama_kantor }}</option>
|
||||
@endforeach
|
||||
</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>
|
||||
@foreach($divisi as $d)
|
||||
<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 filterKantor = document.getElementById('filter-kantor');
|
||||
const filterDivisi = document.getElementById('filter-divisi');
|
||||
const searchPegawai = document.getElementById('search-pegawai');
|
||||
|
||||
if (!selectAllCheckbox) return;
|
||||
|
||||
|
|
@ -234,27 +241,47 @@ function updateCount() {
|
|||
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() {
|
||||
const kantorId = filterKantor.value;
|
||||
const divisiId = filterDivisi.value;
|
||||
const searchQuery = searchPegawai ? searchPegawai.value.toLowerCase().trim() : '';
|
||||
let visibleCount = 0;
|
||||
|
||||
pegawaiItems.forEach(item => {
|
||||
const itemKantor = item.getAttribute('data-kantor');
|
||||
const itemDivisi = item.getAttribute('data-divisi');
|
||||
|
||||
const nameEl = item.querySelector('.font-medium');
|
||||
const nameText = nameEl ? nameEl.textContent.toLowerCase() : '';
|
||||
|
||||
let show = true;
|
||||
if (kantorId && itemKantor !== kantorId) show = false;
|
||||
if (divisiId && itemDivisi !== divisiId) show = false;
|
||||
if (searchQuery && !nameText.includes(searchQuery)) show = false;
|
||||
|
||||
if (show) {
|
||||
item.classList.remove('hidden');
|
||||
visibleCount++;
|
||||
} else {
|
||||
item.classList.add('hidden');
|
||||
const cb = item.querySelector('.user-checkbox');
|
||||
if (cb) cb.checked = false;
|
||||
}
|
||||
});
|
||||
|
||||
updateSelectAllState();
|
||||
updateCount();
|
||||
|
||||
if (visibleCount === 0) {
|
||||
pegawaiContainer.classList.add('hidden');
|
||||
emptyState.classList.remove('hidden');
|
||||
|
|
@ -272,10 +299,12 @@ function filterPegawai() {
|
|||
|
||||
if(filterKantor) filterKantor.addEventListener('change', filterPegawai);
|
||||
if(filterDivisi) filterDivisi.addEventListener('change', filterPegawai);
|
||||
if(searchPegawai) searchPegawai.addEventListener('input', filterPegawai);
|
||||
|
||||
if(pegawaiContainer) {
|
||||
pegawaiContainer.addEventListener('change', function (e) {
|
||||
if (e.target.classList.contains('user-checkbox')) {
|
||||
updateSelectAllState();
|
||||
updateCount();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@
|
|||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<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>
|
||||
|
||||
|
|
|
|||
|
|
@ -232,7 +232,7 @@ class="flex items-center px-4 py-3 rounded-xl transition-all duration-200 group
|
|||
</a>
|
||||
@endif
|
||||
|
||||
{{-- Lembur & Poin – HRD only --}}
|
||||
{{-- Pengajuan Lembur – HRD only --}}
|
||||
@if($isHrd)
|
||||
<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' }}">
|
||||
|
|
@ -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">
|
||||
</path>
|
||||
</svg>
|
||||
<span class="flex-1 font-semibold text-sm">Lembur & Poin</span>
|
||||
<span class="flex-1 font-semibold text-sm">Pengajuan Lembur</span>
|
||||
@php
|
||||
$lemburQuery = \App\Models\Lembur::where('id_status', \App\Enums\StatusPengajuan::PENDING);
|
||||
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) ════════════════════════════════ --}}
|
||||
@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">
|
||||
|
||||
<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
|
||||
</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') }}"
|
||||
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' }}">
|
||||
|
|
|
|||
|
|
@ -13,7 +13,12 @@
|
|||
<div class="space-y-5">
|
||||
<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..."
|
||||
rows="5" required />
|
||||
|
|
|
|||
|
|
@ -15,8 +15,12 @@
|
|||
<x-input label="Judul Pengumuman" name="judul" value="{{ old('judul', $pengumuman->judul) }}"
|
||||
placeholder="Contoh: Libur Hari Raya" required />
|
||||
|
||||
<x-date-input label="Tanggal Tayang" name="tanggal"
|
||||
value="{{ old('tanggal', $pengumuman->tanggal->format('Y-m-d')) }}" 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">
|
||||
{{ $pengumuman->tanggal->translatedFormat('l, d F Y') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<x-textarea label="Isi Pengumuman" name="isi" value="{{ old('isi', $pengumuman->isi) }}"
|
||||
placeholder="Tuliskan detail pengumuman di sini..." rows="5" required />
|
||||
|
|
|
|||
|
|
@ -5,6 +5,12 @@
|
|||
@section('content')
|
||||
|
||||
<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">
|
||||
<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>
|
||||
|
|
@ -14,9 +20,14 @@
|
|||
</x-page-header>
|
||||
|
||||
<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">
|
||||
<x-table>
|
||||
<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">Judul</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>
|
||||
@forelse ($pengumuman as $item)
|
||||
<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">
|
||||
{{ $loop->iteration + ($pengumuman->currentPage() - 1) * $pengumuman->perPage() }}
|
||||
</td>
|
||||
|
|
@ -57,22 +71,114 @@
|
|||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<div class="flex justify-end gap-2">
|
||||
<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" />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@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
|
||||
</x-table>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<x-pagination :paginator="$pengumuman" />
|
||||
</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
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@extends('layouts.app')
|
||||
@extends('layouts.app')
|
||||
|
||||
@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">
|
||||
<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>
|
||||
<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>
|
||||
|
||||
@if(session('error'))
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@extends('layouts.app')
|
||||
@extends('layouts.app')
|
||||
|
||||
@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">
|
||||
<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>
|
||||
<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>
|
||||
|
||||
@if(session('error'))
|
||||
|
|
@ -67,7 +67,7 @@
|
|||
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -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) }}*
|
||||
</span>
|
||||
</div>
|
||||
@elseif(!$p->jam_masuk)
|
||||
<span class="font-mono font-bold text-slate-400">
|
||||
--:--
|
||||
</span>
|
||||
@else
|
||||
<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') }}
|
||||
|
|
|
|||
|
|
@ -17,13 +17,7 @@ class="w-full pl-10 pr-4 py-2 border border-slate-300 rounded-lg focus:outline-n
|
|||
</svg>
|
||||
</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>
|
||||
|
||||
<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>
|
||||
</tr>
|
||||
@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
|
||||
</x-table>
|
||||
<x-pagination :paginator="$roles" />
|
||||
</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">
|
||||
<form id="editForm" method="POST">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<?php
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use App\Http\Controllers\DashboardController;
|
||||
use App\Http\Controllers\PegawaiController;
|
||||
use App\Http\Controllers\DivisiController;
|
||||
|
|
@ -84,6 +85,7 @@
|
|||
Route::post('/presensi/{id}/approve', [PresensiController::class, 'approve'])->name('presensi.approve');
|
||||
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('lembur', LemburController::class)->only(['index', 'update', 'create', 'store']);
|
||||
|
|
@ -119,13 +121,11 @@
|
|||
Route::get('/face-approval', [FaceApprovalController::class, 'index'])->name('face.index');
|
||||
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::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/frame/{userId}/{frameIndex}', [FaceApprovalController::class, 'showFrame'])->name('face.frame');
|
||||
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}/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']);
|
||||
});
|
||||
|
|
@ -160,3 +160,24 @@
|
|||
|
||||
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 |
Loading…
Reference in New Issue