Initial commit for combined ProjectKP

This commit is contained in:
raditarahman5-cloud
2026-06-28 15:14:51 +07:00
commit ac6c3473d4
203 changed files with 41848 additions and 0 deletions
@@ -0,0 +1,49 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class NotificationController extends Controller
{
/**
* Mendapatkan daftar notifikasi user saat ini.
*/
public function index()
{
$user = Auth::user();
if (!$user) {
return response()->json(['success' => false, 'message' => 'Unauthorized'], 401);
}
// Ambil notifikasi (semua), urutkan yang terbaru, batasi 20
$notifications = $user->notifications()->latest()->take(20)->get();
return response()->json([
'success' => true,
'data' => $notifications
]);
}
/**
* Menandai semua notifikasi sebagai telah dibaca.
*/
public function markAsRead()
{
$user = Auth::user();
if (!$user) {
return response()->json(['success' => false, 'message' => 'Unauthorized'], 401);
}
$user->unreadNotifications->markAsRead();
return response()->json([
'success' => true,
'message' => 'Semua notifikasi ditandai telah dibaca.'
]);
}
}
@@ -0,0 +1,390 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Penugasan;
use App\Models\User;
use App\Exports\PenugasanExport;
use Maatwebsite\Excel\Facades\Excel;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Notification;
use App\Notifications\PenugasanStatusNotification;
class PenugasanController extends Controller
{
/**
* 1. DASHBOARD STATS
*/
public function dashboard()
{
try {
$user = Auth::user();
$query = Penugasan::query()->where('is_archived', false);
if ($user && $user->role === 'user') {
$query->whereHas('petugas', function($q) use ($user) {
$q->where('users.id', $user->id);
});
}
$totalTugas = (clone $query)->where('status', '!=', 'dibatalkan')->count();
$proses = (clone $query)->where('status', 'proses')->count();
$selesai = (clone $query)->where('status', 'selesai')->count();
$telahDitinjau = (clone $query)->where('status', 'telah_ditinjau')->count();
$dibatalkan = (clone $query)->where('status', 'dibatalkan')->count();
// Ambil 5 tugas terbaru beserta data petugasnya, kecuali yang dibatalkan
$terbaru = (clone $query)->with('petugas')->where('status', '!=', 'dibatalkan')->latest()->limit(5)->get();
return response()->json([
'success' => true,
'data' => [
'stats' => [
'total_tugas' => $totalTugas,
'status_proses' => $proses,
'status_selesai' => $selesai,
'status_ditinjau' => $telahDitinjau,
'status_batal' => $dibatalkan,
],
'recent_activities' => $terbaru
]
]);
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => $e->getMessage()], 500);
}
}
/**
* 2. LIST SEMUA TUGAS
*/
public function index(Request $request)
{
$user = Auth::user();
$query = Penugasan::with(['petugas', 'pembuat', 'progress.user']);
if ($user && $user->role === 'user') {
$query->whereHas('petugas', function($q) use ($user) {
$q->where('users.id', $user->id);
});
}
if ($request->has('is_archived') && $request->is_archived == 'true') {
$query->where('is_archived', true);
} else {
$query->where('is_archived', false);
}
$data = $query->latest()->get();
return response()->json(['success' => true, 'data' => $data]);
}
/**
* 3. SIMPAN TUGAS BARU (SINKRON DENGAN FRONTEND)
*/
public function store(Request $request)
{
$request->validate([
'nomor_surat' => 'nullable|string|unique:penugasan,nomor_surat',
'kegiatan' => 'required|string',
'tanggal_mulai' => 'required|date',
'pegawai_ids' => 'required|array|min:1',
], [
'nomor_surat.unique' => 'Nomor surat ini sudah terdaftar di sistem (termasuk yang sudah dihapus/dibatalkan). Harap gunakan nomor yang berbeda.',
'pegawai_ids.required' => 'Pilih minimal satu petugas.',
'pegawai_ids.min' => 'Pilih minimal satu petugas.',
]);
DB::beginTransaction();
try {
$adminId = Auth::id() ?? 1;
$lampiranPath = null;
if ($request->hasFile('lampiran')) {
$lampiranPath = $request->file('lampiran')->store('lampiran', 'public');
}
$penugasan = Penugasan::create([
'nomor_surat' => $request->nomor_surat,
'kegiatan' => $request->kegiatan,
'lokasi' => $request->lokasi,
'keterangan' => $request->keterangan,
'lampiran' => $lampiranPath,
'tanggal_mulai' => $request->tanggal_mulai,
'tanggal_selesai' => $request->tanggal_selesai,
'status_note' => $request->status_note,
'status' => 'proses',
'created_by' => $adminId,
]);
// SIMPAN MULTIPLE PEGAWAI
$penugasan->petugas()->sync($request->pegawai_ids);
DB::commit();
return response()->json([
'success' => true,
'message' => 'Penugasan berhasil dibuat!',
'data' => $penugasan->load('petugas')
], 201);
} catch (\Exception $e) {
DB::rollback();
return response()->json([
'success' => false,
'message' => $e->getMessage()
], 500);
}
}
/**
* 3b. SHOW DETAIL TUGAS
*/
public function show($id)
{
try {
$penugasan = Penugasan::with(['petugas', 'pembuat', 'penyetuju', 'progress.user'])->findOrFail($id);
return response()->json([
'success' => true,
'data' => $penugasan
]);
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => 'Data tidak ditemukan: ' . $e->getMessage()], 404);
}
}
/**
* 3c. UPDATE TUGAS
*/
public function update(Request $request, $id)
{
$request->validate([
'nomor_surat' => 'nullable|string|unique:penugasan,nomor_surat,' . $id,
'kegiatan' => 'required|string',
'tanggal_mulai' => 'required|date',
'pegawai_ids' => 'required|array|min:1',
], [
'nomor_surat.unique' => 'Nomor surat ini sudah terdaftar di sistem (termasuk yang sudah dihapus/dibatalkan). Harap gunakan nomor yang berbeda.',
'pegawai_ids.required' => 'Pilih minimal satu petugas.',
'pegawai_ids.min' => 'Pilih minimal satu petugas.',
]);
DB::beginTransaction();
try {
$penugasan = Penugasan::findOrFail($id);
$lampiranPath = $penugasan->lampiran;
if ($request->hasFile('lampiran')) {
if ($lampiranPath && Storage::disk('public')->exists($lampiranPath)) {
Storage::disk('public')->delete($lampiranPath);
}
$lampiranPath = $request->file('lampiran')->store('lampiran', 'public');
}
$penugasan->update([
'nomor_surat' => $request->nomor_surat,
'kegiatan' => $request->kegiatan,
'lokasi' => $request->lokasi,
'keterangan' => $request->keterangan,
'lampiran' => $lampiranPath,
'tanggal_mulai' => $request->tanggal_mulai,
'tanggal_selesai' => $request->tanggal_selesai,
'status_note' => $request->status_note,
// tidak update status/created_by
]);
$penugasan->petugas()->sync($request->pegawai_ids);
DB::commit();
return response()->json([
'success' => true,
'message' => 'Penugasan berhasil diperbarui!',
'data' => $penugasan->load('petugas')
]);
} catch (\Exception $e) {
DB::rollback();
return response()->json(['success' => false, 'message' => $e->getMessage()], 500);
}
}
/**
* 4. UPDATE STATUS (TINJAU)
*/
public function updateStatus(Request $request, $id)
{
try {
$penugasan = Penugasan::findOrFail($id);
if ($request->status == 'telah_ditinjau') {
$penugasan->update([
'status' => 'telah_ditinjau',
'approved_by' => Auth::id() ?? $request->approved_by,
'approved_at' => now(),
'status_note' => $request->has('status_note') ? $request->status_note : null
]);
} else {
$updateData = ['status' => $request->status];
if ($request->has('status_note')) {
$updateData['status_note'] = $request->status_note;
}
$penugasan->update($updateData);
// Kirim notifikasi
if (in_array($request->status, ['selesai', 'dibatalkan', 'proses'])) {
$action = 'diperbarui';
if ($request->status === 'selesai') $action = 'disetujui';
if ($request->status === 'dibatalkan') $action = 'dibatalkan';
if ($request->status === 'proses') $action = 'dikembalikan (revisi)';
if ($penugasan->petugas->isNotEmpty()) {
Notification::send($penugasan->petugas, new PenugasanStatusNotification($penugasan, $request->status, $action));
}
}
}
return response()->json(['success' => true, 'message' => 'Status diperbarui!']);
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => $e->getMessage()], 500);
}
}
/**
* 5. BATALKAN TUGAS
*/
public function destroy(Request $request, $id)
{
$request->validate([
'alasan' => 'required|string' // Frontend kirim 'alasan'
]);
try {
$penugasan = Penugasan::findOrFail($id);
$penugasan->update([
'status' => 'dibatalkan',
'alasan_hapus' => $request->alasan
]);
if ($penugasan->petugas->isNotEmpty()) {
Notification::send($penugasan->petugas, new PenugasanStatusNotification($penugasan, 'dibatalkan', 'dihapus'));
}
return response()->json([
'success' => true,
'message' => 'Kegiatan berhasil dibatalkan.'
]);
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => $e->getMessage()], 500);
}
}
/**
* 6. NOTIFIKASI
*/
public function notifications()
{
// Contoh implementasi: kirim kegiatan yang sudah 'telah_ditinjau' untuk user ini
try {
$user = Auth::user();
$notifications = [];
if ($user && $user->role === 'user') {
// Pegawai id mungkin sama dengan user_id
$pegawai = \App\Models\Pegawai::where('user_id', $user->id)->first();
if ($pegawai) {
$notifications = $pegawai->penugasan()
->where('status', 'telah_ditinjau')
->orderBy('approved_at', 'desc')
->limit(5)
->get()
->map(function($p) {
return [
'id' => $p->id,
'text' => "Penugasan '{$p->kegiatan}' telah ditinjau.",
'is_read' => false
];
});
}
}
return response()->json([
'success' => true,
'data' => $notifications
]);
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => $e->getMessage()], 500);
}
}
/**
* 7. EXCEL EXPORT
*/
public function export(Request $request)
{
try {
$columns = $request->input('columns', []);
return Excel::download(new PenugasanExport($columns), 'Laporan_Penugasan.xlsx');
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => 'Export Gagal: ' . $e->getMessage()], 500);
}
}
public function toggleArchive($id)
{
try {
$penugasan = Penugasan::findOrFail($id);
$penugasan->is_archived = !$penugasan->is_archived;
$penugasan->save();
$status = $penugasan->is_archived ? 'diarsipkan' : 'dikembalikan dari arsip';
return response()->json([
'success' => true,
'message' => "Laporan berhasil $status."
]);
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => 'Gagal mengarsipkan laporan: ' . $e->getMessage()], 500);
}
}
public function storeProgress(Request $request, $id)
{
try {
$request->validate([
'note' => 'required|string',
'attachment' => 'nullable|file|mimes:jpg,jpeg,png,pdf|max:2048'
]);
$penugasan = Penugasan::findOrFail($id);
$path = null;
if ($request->hasFile('attachment')) {
$file = $request->file('attachment');
$filename = time() . '_' . $file->getClientOriginalName();
$file->move(public_path('uploads/progress'), $filename);
$path = 'uploads/progress/' . $filename;
}
$progress = $penugasan->progress()->create([
'user_id' => Auth::id(),
'note' => $request->note,
'attachment' => $path
]);
return response()->json([
'success' => true,
'message' => 'Progress log added successfully',
'data' => $progress->load('user')
]);
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => 'Failed to add progress: ' . $e->getMessage()], 500);
}
}
}
@@ -0,0 +1,200 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rule;
use App\Models\User;
class AuthController extends Controller
{
/**
* LOGIN API
* Menerima username/email + password.
* Mengembalikan token untuk dipakai frontend KP-ASOY.
*/
public function login(Request $request)
{
$request->validate([
'username' => 'nullable|string',
'login' => 'nullable|string',
'password' => 'required|string',
]);
// Frontend KP-ASOY mengirim field "username".
// Tapi tetap kita dukung field "login" agar fleksibel.
$login = trim($request->username ?? $request->login);
if (!$login) {
return response()->json([
'message' => 'Username atau email wajib diisi.'
], 422);
}
// Cek apakah input berupa email atau username
$field = filter_var($login, FILTER_VALIDATE_EMAIL) ? 'email' : 'username';
$user = User::where($field, $login)->first();
\Illuminate\Support\Facades\Log::info('Login Attempt', ['login' => $login, 'password' => $request->password, 'user_found' => $user ? true : false, 'hash_check' => $user ? Hash::check($request->password, $user->password) : false]);
if (!$user || !Hash::check($request->password, $user->password)) {
return response()->json([
'message' => 'Login gagal. Username/email atau password salah.'
], 401);
}
// Hapus token lama agar tidak menumpuk terlalu banyak
$user->tokens()->delete();
// Buat token Sanctum
$token = $user->createToken('kp-asoy-token')->plainTextToken;
return response()->json([
'message' => 'Login berhasil',
'token' => $token,
'user' => [
'id' => $user->id,
'name' => $user->name,
'username' => $user->username,
'email' => $user->email,
'role' => strtolower($user->role),
'jabatan' => $user->jabatan,
]
], 200);
}
/**
* REGISTER USER
* Dipakai admin/superadmin untuk membuat akun user/pegawai.
*/
public function register(Request $request)
{
$request->validate([
'name' => 'required|string|max:255',
'username' => 'required|string|max:255|unique:users,username',
'password' => 'required|string|min:6',
'role' => [
'required',
Rule::in(['user', 'admin', 'superadmin', 'User', 'Admin', 'Superadmin', 'Super Admin'])
],
'jabatan' => 'nullable|string|max:255',
]);
$role = strtolower($request->role);
if ($role === 'super admin') {
$role = 'superadmin';
}
// Generate email if not provided to satisfy DB unique constraint
$email = $request->email ?? ($request->username . '@mail.com');
$user = User::create([
'name' => $request->name,
'username' => $request->username,
'email' => $email,
'password' => bcrypt($request->password),
'role' => $role,
'jabatan' => $request->jabatan,
]);
return response()->json([
'message' => 'Register berhasil',
'user' => [
'id' => $user->id,
'name' => $user->name,
'username' => $user->username,
'email' => $user->email,
'role' => $user->role,
'jabatan' => $user->jabatan,
]
], 201);
}
/**
* DATA USER YANG SEDANG LOGIN
*/
public function me(Request $request)
{
$user = $request->user();
return response()->json([
'user' => [
'id' => $user->id,
'name' => $user->name,
'username' => $user->username,
'email' => $user->email,
'role' => strtolower($user->role),
'jabatan' => $user->jabatan,
]
]);
}
/**
* LOGOUT API
*/
public function logout(Request $request)
{
if ($request->user()) {
$request->user()->currentAccessToken()->delete();
}
return response()->json([
'message' => 'Logout berhasil'
]);
}
/**
* UPDATE AKUN
* Untuk fitur pengaturan akun: ubah email dan password.
*/
public function updateProfile(Request $request)
{
$user = $request->user();
$request->validate([
'username' => [
'required',
'string',
Rule::unique('users', 'username')->ignore($user->id),
],
'password' => 'nullable|string|min:6',
'current_password' => 'nullable|string',
]);
$user->username = $request->username;
// Jika tidak ada email, tidak perlu diupdate.
// Backend tetap aman karena saat register kita yang men-generate email.
if ($request->filled('password')) {
if ($user->role === 'user') {
return response()->json([
'message' => 'Pegawai tidak diizinkan mengubah kata sandi.'
], 403);
}
if (!Hash::check($request->current_password, $user->password)) {
return response()->json([
'message' => 'Kata sandi saat ini tidak cocok.'
], 400);
}
$user->password = bcrypt($request->password);
}
$user->save();
return response()->json([
'message' => 'Profil berhasil diperbarui',
'user' => [
'id' => $user->id,
'name' => $user->name,
'username' => $user->username,
'email' => $user->email,
'role' => strtolower($user->role),
'jabatan' => $user->jabatan,
]
]);
}
}
@@ -0,0 +1,8 @@
<?php
namespace App\Http\Controllers;
abstract class Controller
{
//
}
@@ -0,0 +1,156 @@
<?php
namespace App\Http\Controllers;
use App\Models\Pegawai;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
class PegawaiController extends Controller
{
// 1. Tampil Semua Data
public function index()
{
$data = Pegawai::with('user:id,email,role')->get();
return response()->json(['status' => true, 'data' => $data], 200);
}
// 2. Simpan Data Baru (Oleh Admin/Super Admin)
public function store(Request $request)
{
$validator = Validator::make($request->all(), [
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users,email',
'password' => 'required|string|min:6',
'nip' => 'required|string|unique:pegawai,nip',
'nama' => 'required|string|max:255',
'jabatan' => 'required|string|max:255',
'role' => 'nullable|in:super_admin,admin,pegawai',
]);
if ($validator->fails()) {
return response()->json(['status' => false, 'errors' => $validator->errors()], 422);
}
DB::beginTransaction();
try {
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
'role' => $request->role ?? 'pegawai',
]);
Pegawai::create([
'nip' => $request->nip,
'nama' => $request->nama,
'jabatan' => $request->jabatan,
'user_id' => $user->id,
]);
DB::commit();
return response()->json(['status' => true, 'message' => 'Pegawai berhasil ditambah'], 201);
} catch (\Exception $e) {
DB::rollBack();
return response()->json(['status' => false, 'error' => $e->getMessage()], 500);
}
}
// 3. Lihat Detail
public function show($id)
{
$pegawai = Pegawai::with('user:id,email,role')->find($id);
if (!$pegawai) return response()->json(['status' => false, 'message' => 'Data tidak ditemukan'], 404);
return response()->json(['status' => true, 'data' => $pegawai], 200);
}
// 4. Update Data
public function update(Request $request, $id)
{
$pegawai = Pegawai::find($id);
if (!$pegawai) return response()->json(['message' => 'Data tidak ditemukan'], 404);
DB::beginTransaction();
try {
$pegawai->update([
'nip' => $request->nip ?? $pegawai->nip,
'nama' => $request->nama ?? $pegawai->nama,
'jabatan' => $request->jabatan ?? $pegawai->jabatan,
]);
$user = User::find($pegawai->user_id);
if ($user) {
$user->update([
'name' => $request->nama ?? $user->name,
'role' => $request->role ?? $user->role,
]);
}
DB::commit();
return response()->json(['status' => true, 'message' => 'Data berhasil diupdate']);
} catch (\Exception $e) {
DB::rollBack();
return response()->json(['status' => false, 'error' => $e->getMessage()], 500);
}
}
/**
* 5. HAPUS PEGAWAI (REVISI SESUAI REQUEST DOSEN)
* Ditambahkan input 'alasan_hapus'
*/
public function destroy(Request $request, $id)
{
$pegawai = Pegawai::find($id);
if (!$pegawai) return response()->json(['message' => 'Data tidak ditemukan'], 404);
// Validasi alasan hapus (Request Dosen)
$request->validate([
'alasan_hapus' => 'required|string'
]);
// Simpan alasan ke database sebelum di-soft delete
$pegawai->update([
'alasan_hapus' => $request->alasan_hapus
]);
$pegawai->delete();
return response()->json([
'status' => true,
'message' => 'Pegawai berhasil dipindahkan ke tempat sampah dengan alasan: ' . $request->alasan_hapus
]);
}
// 6. Melihat "Tong Sampah" (Sesuai request dosen: Dashboard tidak perlu detail, detail di sini)
public function trash()
{
// Ambil data yang sudah di-soft delete beserta alasan hapusnya
$pegawaiTerhapus = Pegawai::onlyTrashed()->get();
return response()->json([
'status' => true,
'data' => $pegawaiTerhapus
]);
}
// 7. Restore
public function restore($id)
{
$pegawai = Pegawai::withTrashed()->find($id);
if ($pegawai && $pegawai->trashed()) {
// Kosongkan kembali alasan hapus saat di-restore
$pegawai->update(['alasan_hapus' => null]);
$pegawai->restore();
return response()->json([
'status' => true,
'message' => 'Data pegawai ' . $pegawai->nama . ' berhasil dikembalikan!'
]);
}
return response()->json(['status' => false, 'message' => 'Data tidak ditemukan di tong sampah'], 404);
}
}
@@ -0,0 +1,63 @@
<?php
namespace App\Http\Controllers;
use App\Models\UnitKerja;
use Illuminate\Http\Request;
class UnitKerjaController extends Controller
{
public function index()
{
$data = UnitKerja::all();
return response()->json([
'success' => true,
'data' => $data
]);
}
public function store(Request $request)
{
$request->validate([
'nama_unit' => 'required|unique:unit_kerja,nama_unit',
]);
$unit = UnitKerja::create($request->all());
return response()->json([
'success' => true,
'message' => 'Unit Kerja berhasil ditambah',
'data' => $unit
]);
}
public function show($id)
{
$unit = UnitKerja::find($id);
if (!$unit) return response()->json(['message' => 'Data tidak ditemukan'], 404);
return response()->json(['success' => true, 'data' => $unit]);
}
public function update(Request $request, $id)
{
$unit = UnitKerja::find($id);
if (!$unit) return response()->json(['message' => 'Data tidak ditemukan'], 404);
$unit->update($request->all());
return response()->json([
'success' => true,
'message' => 'Unit Kerja berhasil diupdate'
]);
}
public function destroy($id)
{
$unit = UnitKerja::find($id);
if (!$unit) return response()->json(['message' => 'Data tidak ditemukan'], 404);
$unit->delete();
return response()->json(['success' => true, 'message' => 'Unit Kerja dihapus']);
}
}
@@ -0,0 +1,29 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class CheckRole
{
/**
* Handle an incoming request.
*
* @param Closure(Request): (Response) $next
*/
public function handle(Request $request, Closure $next, ...$roles)
{
// Cek apakah user sudah login dan rolenya ada di daftar yang diizinkan
if (!$request->user() || !in_array($request->user()->role, $roles)) {
return response()->json([
'status' => false,
'message' => 'Akses ditolak! Anda tidak punya hak akses untuk aksi ini.'
], 403); // 403 artinya Forbidden
}
return $next($request);
}
}