sedikit penyesuaian
This commit is contained in:
parent
96bf631b45
commit
b2c5d494ab
|
|
@ -99,23 +99,24 @@ public function verifyFace(Request $request)
|
|||
'tipe' => 'nullable|string|in:presensi,test',
|
||||
]);
|
||||
|
||||
$tipe = $request->input('tipe', 'presensi');
|
||||
|
||||
try {
|
||||
set_time_limit(0);
|
||||
|
||||
$user = $request->user();
|
||||
$tipe = $request->input('tipe', 'presensi');
|
||||
|
||||
$file = $request->file('foto');
|
||||
$result = $this->faceService->verifyFace($user->id, $file);
|
||||
|
||||
\DB::table('log_verifikasi_wajah')->insert([
|
||||
'id_user' => $user->id,
|
||||
'confidence' => $result['confidence'] ?? null,
|
||||
'svm_confidence' => $result['svm_confidence'] ?? null,
|
||||
'normalized_distance' => $result['normalized_distance'] ?? null,
|
||||
'verification_status' => $result['verification_status'] ?? null,
|
||||
'is_match' => $result['verified'] ?? false,
|
||||
'blur_score' => $result['blur_score'] ?? null,
|
||||
'skor_kepercayaan' => $result['confidence'] ?? null,
|
||||
'skor_svm' => $result['svm_confidence'] ?? null,
|
||||
'jarak_normalisasi' => $result['normalized_distance'] ?? null,
|
||||
'status_verifikasi' => $result['verification_status'] ?? null,
|
||||
'apakah_cocok' => $result['verified'] ?? false,
|
||||
'skor_blur' => $result['blur_score'] ?? null,
|
||||
'tipe' => $tipe,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
|
|
@ -131,9 +132,9 @@ public function verifyFace(Request $request)
|
|||
if (isset($user)) {
|
||||
\DB::table('log_verifikasi_wajah')->insert([
|
||||
'id_user' => $user->id,
|
||||
'confidence' => 0,
|
||||
'verification_status' => 'ERROR',
|
||||
'is_match' => false,
|
||||
'skor_kepercayaan' => 0,
|
||||
'status_verifikasi' => 'ERROR',
|
||||
'apakah_cocok' => false,
|
||||
'tipe' => $tipe ?? 'presensi',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
|
|
|
|||
|
|
@ -2,26 +2,34 @@
|
|||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Enums\JenisPengurangan;
|
||||
use App\Enums\StatusPengajuan;
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Poin;
|
||||
use App\Models\PenggunaanPoin;
|
||||
use App\Services\NotifikasiService;
|
||||
use App\Services\PoinService;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class PoinController extends Controller
|
||||
{
|
||||
protected $poinService;
|
||||
|
||||
public function __construct(PoinService $poinService)
|
||||
{
|
||||
$this->poinService = $poinService;
|
||||
}
|
||||
|
||||
public function getExpiringPoints()
|
||||
{
|
||||
try {
|
||||
$userId = auth()->id();
|
||||
$userId = Auth::id();
|
||||
|
||||
$poinService = new PoinService();
|
||||
$poinExpiring = $poinService->getExpiringPoints($userId);
|
||||
$activePoints = $poinService->getActivePoints($userId);
|
||||
$poinExpiring = $this->poinService->getExpiringPoints($userId);
|
||||
$activePoints = $this->poinService->getActivePoints($userId);
|
||||
|
||||
$firstExpiring = $poinExpiring->first();
|
||||
$formattedExpiring = null;
|
||||
|
|
@ -46,10 +54,8 @@ public function getExpiringPoints()
|
|||
public function getPointHistory()
|
||||
{
|
||||
try {
|
||||
$userId = auth()->id();
|
||||
|
||||
$poinService = new PoinService();
|
||||
$history = $poinService->getPointHistory($userId);
|
||||
$userId = Auth::id();
|
||||
$history = $this->poinService->getPointHistory($userId);
|
||||
|
||||
return ApiResponse::success(['history' => $history]);
|
||||
|
||||
|
|
@ -62,10 +68,10 @@ public function redeem(Request $request)
|
|||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'jumlah' => 'required|integer|min:1',
|
||||
'keterangan' => 'required|string',
|
||||
'keterangan' => 'required|string|max:500',
|
||||
'id_pengurangan' => 'required|integer',
|
||||
'jam_masuk_custom' => 'required_if:id_pengurangan,4|date_format:H:i|nullable',
|
||||
'jam_pulang_custom' => 'required_if:id_pengurangan,5|date_format:H:i|nullable',
|
||||
'jam_masuk_custom' => 'required_if:id_pengurangan,' . JenisPengurangan::MASUK_SIANG_POIN . '|date_format:H:i|nullable',
|
||||
'jam_pulang_custom' => 'required_if:id_pengurangan,' . JenisPengurangan::PULANG_CEPAT_POIN . '|date_format:H:i|nullable',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
|
|
@ -73,18 +79,16 @@ public function redeem(Request $request)
|
|||
}
|
||||
|
||||
try {
|
||||
$userId = auth()->id();
|
||||
$userId = Auth::id();
|
||||
$jumlah = $request->jumlah;
|
||||
$keterangan = $request->keterangan;
|
||||
|
||||
$poinService = new PoinService();
|
||||
$totalPoin = $poinService->getActivePoints($userId);
|
||||
$totalPoin = $this->poinService->getActivePoints($userId);
|
||||
|
||||
if ($totalPoin < $jumlah) {
|
||||
return ApiResponse::error('Saldo poin tidak mencukupi', 400);
|
||||
return ApiResponse::error('Saldo poin tidak mencukupi. Saldo aktif: ' . $totalPoin . ', dibutuhkan: ' . $jumlah, 400);
|
||||
}
|
||||
|
||||
preg_match('/\[(\d{4}-\d{2}-\d{2})\]/', $keterangan, $matches);
|
||||
preg_match('/\[(\d{4}-\d{2}-\d{2})\]/', $request->keterangan, $matches);
|
||||
$tanggalPenggunaan = isset($matches[1]) ? $matches[1] : now()->format('Y-m-d');
|
||||
|
||||
$jadwal = \App\Models\JadwalKerja::with('shift')
|
||||
|
|
@ -113,6 +117,14 @@ public function redeem(Request $request)
|
|||
'tanggal_diajukan' => now(),
|
||||
]);
|
||||
|
||||
$user = Auth::user();
|
||||
app(NotifikasiService::class)->kirimKeRole(
|
||||
'hrd',
|
||||
'pengajuan_baru',
|
||||
'Pengajuan Penggunaan Poin',
|
||||
$user->nama_lengkap . ' mengajukan penggunaan ' . $jumlah . ' poin.'
|
||||
);
|
||||
|
||||
return ApiResponse::success(null, 'Pengajuan penukaran poin berhasil disimpan');
|
||||
} catch (\Exception $e) {
|
||||
return ApiResponse::error($e->getMessage(), 400);
|
||||
|
|
|
|||
|
|
@ -258,8 +258,15 @@ public function history(Request $request)
|
|||
try {
|
||||
$masuk = Carbon::parse($item->tanggal . ' ' . $item->jam_masuk);
|
||||
$pulang = Carbon::parse($item->tanggal . ' ' . $item->jam_pulang);
|
||||
$diff = $masuk->diff($pulang);
|
||||
$totalJam = $diff->h . 'j ' . $diff->i . 'm';
|
||||
|
||||
if ($pulang->lessThan($masuk)) {
|
||||
$pulang->addDay();
|
||||
}
|
||||
|
||||
$diffMinutes = $pulang->diffInMinutes($masuk);
|
||||
$hours = floor($diffMinutes / 60);
|
||||
$minutes = $diffMinutes % 60;
|
||||
$totalJam = $hours . 'j ' . $minutes . 'm';
|
||||
} catch (\Exception $e) {
|
||||
$totalJam = '-';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\HariLibur;
|
||||
use App\Http\Requests\StoreHariLiburRequest;
|
||||
use Illuminate\Http\Request;
|
||||
use Carbon\Carbon;
|
||||
|
||||
|
|
@ -27,15 +28,9 @@ public function index(Request $request)
|
|||
return view('hari_libur.index', compact('hariLiburs', 'kantors'));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
public function store(StoreHariLiburRequest $request)
|
||||
{
|
||||
$request->validate([
|
||||
'tanggal' => 'required|date',
|
||||
'keterangan' => 'required|string|max:255',
|
||||
'id_kantor' => 'nullable|exists:kantor,id_kantor',
|
||||
]);
|
||||
|
||||
HariLibur::create($request->all());
|
||||
HariLibur::create($request->validated());
|
||||
|
||||
return redirect()->route('hari-libur.index')
|
||||
->with('success', 'Hari libur berhasil ditambahkan.');
|
||||
|
|
@ -44,13 +39,13 @@ public function store(Request $request)
|
|||
public function update(Request $request, $id)
|
||||
{
|
||||
$request->validate([
|
||||
'tanggal' => 'required|date',
|
||||
'keterangan' => 'required|string|max:255',
|
||||
'id_kantor' => 'nullable|exists:kantor,id_kantor',
|
||||
'tanggal' => 'required|date',
|
||||
'keterangan' => 'required|string|max:255',
|
||||
'id_kantor' => 'nullable|exists:kantor,id_kantor',
|
||||
]);
|
||||
|
||||
$hariLibur = HariLibur::where('id', '=', $id)->first() ?? HariLibur::where('id_hari_libur', '=', $id)->firstOrFail();
|
||||
$hariLibur->update($request->all());
|
||||
$hariLibur = HariLibur::findOrFail($id);
|
||||
$hariLibur->update($request->only(['tanggal', 'keterangan', 'id_kantor']));
|
||||
|
||||
return redirect()->route('hari-libur.index')
|
||||
->with('success', 'Hari libur berhasil diperbarui.');
|
||||
|
|
@ -58,8 +53,7 @@ public function update(Request $request, $id)
|
|||
|
||||
public function destroy($id)
|
||||
{
|
||||
$hariLibur = HariLibur::where('id', '=', $id)->first() ?? HariLibur::where('id_hari_libur', '=', $id)->firstOrFail();
|
||||
$hariLibur->delete();
|
||||
HariLibur::findOrFail($id)->delete();
|
||||
|
||||
return redirect()->route('hari-libur.index')
|
||||
->with('success', 'Hari libur berhasil dihapus.');
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@
|
|||
use App\Models\PenggunaanPoin;
|
||||
use App\Services\PoinService;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use App\Http\Requests\StoreJadwalRequest;
|
||||
use App\Http\Requests\BulkDestroyJadwalRequest;
|
||||
|
||||
class JadwalController extends Controller
|
||||
{
|
||||
|
|
@ -28,7 +30,7 @@ public function index()
|
|||
return $s;
|
||||
});
|
||||
|
||||
$pegawaiQuery = User::where('status_aktif', 1);
|
||||
$pegawaiQuery = User::where('status_aktif', 1)->bukanHrd();
|
||||
if (!$isGlobalAdmin) {
|
||||
$pegawaiQuery->where('id_kantor', $user->id_kantor);
|
||||
}
|
||||
|
|
@ -211,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)->with(['kantor', 'jabatan']);
|
||||
$pegawaiQuery = User::where('status_aktif', 1)->bukanHrd()->with(['kantor', 'jabatan']);
|
||||
if (!$isGlobalAdmin) {
|
||||
$pegawaiQuery->where('id_kantor', $user->id_kantor);
|
||||
}
|
||||
|
|
@ -220,15 +222,9 @@ public function create()
|
|||
return view('jadwal.create', compact('shifts', 'pegawai', 'kantor', 'divisi'));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
public function store(StoreJadwalRequest $request)
|
||||
{
|
||||
$request->validate([
|
||||
'tanggal_mulai' => 'required|date',
|
||||
'tanggal_selesai' => 'required|date|after_or_equal:tanggal_mulai',
|
||||
'id_shift' => 'required|exists:shift_kerja,id_shift',
|
||||
'user_ids' => 'required|array',
|
||||
'user_ids.*' => 'exists:users,id',
|
||||
]);
|
||||
$data = $request->validated();
|
||||
|
||||
// Guard: pastikan ada data shift sebelum memproses
|
||||
if (ShiftKerja::count() === 0) {
|
||||
|
|
@ -249,12 +245,12 @@ public function store(Request $request)
|
|||
}
|
||||
}
|
||||
|
||||
$startDate = Carbon::parse($request->tanggal_mulai);
|
||||
$endDate = Carbon::parse($request->tanggal_selesai);
|
||||
$startDate = Carbon::parse($data['tanggal_mulai']);
|
||||
$endDate = Carbon::parse($data['tanggal_selesai']);
|
||||
$period = \Carbon\CarbonPeriod::create($startDate, $endDate);
|
||||
|
||||
$shiftId = $request->id_shift;
|
||||
$userIds = $request->user_ids;
|
||||
$shiftId = $data['id_shift'];
|
||||
$userIds = $data['user_ids'];
|
||||
|
||||
$count = 0;
|
||||
$refundCount = 0;
|
||||
|
|
@ -364,38 +360,33 @@ public function destroy($id)
|
|||
return response()->json(['success' => true, 'message' => 'Jadwal berhasil dihapus']);
|
||||
}
|
||||
|
||||
public function bulkDestroy(Request $request)
|
||||
public function bulkDestroy(BulkDestroyJadwalRequest $request)
|
||||
{
|
||||
$request->validate([
|
||||
'tanggal_mulai' => 'required|date',
|
||||
'tanggal_selesai' => 'required|date|after_or_equal:tanggal_mulai',
|
||||
'user_ids' => 'required|array',
|
||||
'user_ids.*' => 'exists:users,id',
|
||||
]);
|
||||
$data = $request->validated();
|
||||
|
||||
$user = Auth::user();
|
||||
$isGlobalAdmin = $user->isGlobalAdmin();
|
||||
|
||||
if (!$isGlobalAdmin) {
|
||||
$invalidUsers = User::whereIn('id', $request->user_ids)
|
||||
$invalidUsers = User::whereIn('id', $data['user_ids'])
|
||||
->where('id_kantor', '!=', $user->id_kantor)
|
||||
->exists();
|
||||
|
||||
if ($invalidUsers) {
|
||||
return redirect()->back()->with('error', 'Anda tidak diizinkan menghapus jadwal karyawan di luar kantor Anda.');
|
||||
return redirect()->route('jadwal.index')->with('error', 'Anda tidak diizinkan menghapus jadwal karyawan di luar kantor Anda.');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
DB::transaction(function () use ($request) {
|
||||
JadwalKerja::whereIn('id_user', $request->user_ids)
|
||||
->whereBetween('tanggal', [$request->tanggal_mulai, $request->tanggal_selesai])
|
||||
DB::transaction(function () use ($data) {
|
||||
JadwalKerja::whereIn('id_user', $data['user_ids'])
|
||||
->whereBetween('tanggal', [$data['tanggal_mulai'], $data['tanggal_selesai']])
|
||||
->delete();
|
||||
});
|
||||
|
||||
return redirect()->back()->with('success', 'Jadwal kerja berhasil dihapus pada rentang tanggal tersebut.');
|
||||
return redirect()->route('jadwal.index')->with('success', 'Jadwal kerja berhasil dihapus pada rentang tanggal tersebut.');
|
||||
} catch (\Exception $e) {
|
||||
return redirect()->back()->with('error', 'Terjadi kesalahan saat menghapus jadwal: ' . $e->getMessage());
|
||||
return redirect()->route('jadwal.index')->with('error', 'Terjadi kesalahan saat menghapus jadwal: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
use App\Models\User;
|
||||
use App\Models\Presensi;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Carbon\Carbon;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
|
|
@ -17,11 +18,15 @@ class LaporanController extends Controller
|
|||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$bulan = $request->input('bulan', date('m'));
|
||||
$tahun = $request->input('tahun', date('Y'));
|
||||
$bulan = $request->input('bulan', date('m'));
|
||||
$tahun = $request->input('tahun', date('Y'));
|
||||
$divisiId = $request->input('id_divisi');
|
||||
|
||||
$rekap = $this->buildRekap($bulan, $tahun, $divisiId);
|
||||
$authUser = Auth::user();
|
||||
$isGlobalAdmin = $authUser->isGlobalAdmin();
|
||||
$kantorId = $isGlobalAdmin ? null : $authUser->id_kantor;
|
||||
|
||||
$rekap = $this->buildRekap($bulan, $tahun, $divisiId, $kantorId);
|
||||
$divisiList = \App\Models\Divisi::all();
|
||||
|
||||
return view('laporan.index', compact('rekap', 'bulan', 'tahun', 'divisiList', 'divisiId'));
|
||||
|
|
@ -29,15 +34,24 @@ public function index(Request $request)
|
|||
|
||||
public function cuti(Request $request)
|
||||
{
|
||||
$bulan = $request->input('bulan', date('m'));
|
||||
$tahun = $request->input('tahun', date('Y'));
|
||||
$search = $request->input('search');
|
||||
$bulan = $request->input('bulan', date('m'));
|
||||
$tahun = $request->input('tahun', date('Y'));
|
||||
$search = $request->input('search');
|
||||
$divisiId = $request->input('id_divisi');
|
||||
|
||||
$authUser = Auth::user();
|
||||
$isGlobalAdmin = $authUser->isGlobalAdmin();
|
||||
|
||||
$query = \App\Models\PengajuanIzin::with(['user.divisi', 'jenisIzin'])
|
||||
->whereYear('tanggal_mulai', $tahun)
|
||||
->whereMonth('tanggal_mulai', $bulan);
|
||||
|
||||
if (!$isGlobalAdmin) {
|
||||
$query->whereHas('user', function ($q) use ($authUser) {
|
||||
$q->where('id_kantor', $authUser->id_kantor);
|
||||
});
|
||||
}
|
||||
|
||||
if ($search) {
|
||||
$query->whereHas('user', function($q) use ($search) {
|
||||
$q->where('nama_lengkap', 'like', "%{$search}%")
|
||||
|
|
@ -51,7 +65,7 @@ public function cuti(Request $request)
|
|||
});
|
||||
}
|
||||
|
||||
$izinList = $query->orderBy('tanggal_mulai', 'desc')->paginate(15)->withQueryString();
|
||||
$izinList = $query->orderBy('tanggal_mulai', 'desc')->paginate(15)->withQueryString();
|
||||
$divisiList = \App\Models\Divisi::all();
|
||||
|
||||
return view('laporan.izin', compact('izinList', 'bulan', 'tahun', 'search', 'divisiId', 'divisiList'));
|
||||
|
|
@ -64,10 +78,19 @@ public function exportIzinExcel(Request $request)
|
|||
$search = $request->input('search');
|
||||
$divisiId = $request->input('id_divisi');
|
||||
|
||||
$authUser = Auth::user();
|
||||
$isGlobalAdmin = $authUser->isGlobalAdmin();
|
||||
|
||||
$query = \App\Models\PengajuanIzin::with(['user.divisi', 'jenisIzin'])
|
||||
->whereYear('tanggal_mulai', $tahun)
|
||||
->whereMonth('tanggal_mulai', $bulan);
|
||||
|
||||
if (!$isGlobalAdmin) {
|
||||
$query->whereHas('user', function ($q) use ($authUser) {
|
||||
$q->where('id_kantor', $authUser->id_kantor);
|
||||
});
|
||||
}
|
||||
|
||||
if ($search) {
|
||||
$query->whereHas('user', function ($q) use ($search) {
|
||||
$q->where('nama_lengkap', 'like', "%{$search}%")
|
||||
|
|
@ -94,11 +117,14 @@ public function exportIzinExcel(Request $request)
|
|||
|
||||
public function exportExcel(Request $request)
|
||||
{
|
||||
$bulan = $request->input('bulan', date('m'));
|
||||
$tahun = $request->input('tahun', date('Y'));
|
||||
$bulan = $request->input('bulan', date('m'));
|
||||
$tahun = $request->input('tahun', date('Y'));
|
||||
$divisiId = $request->input('id_divisi');
|
||||
|
||||
$rekap = $this->buildRekap($bulan, $tahun, $divisiId);
|
||||
$authUser = Auth::user();
|
||||
$kantorId = $authUser->isGlobalAdmin() ? null : $authUser->id_kantor;
|
||||
|
||||
$rekap = $this->buildRekap($bulan, $tahun, $divisiId, $kantorId);
|
||||
|
||||
if (empty($rekap)) {
|
||||
return redirect()->back()->with('error', 'Tidak ada data presensi untuk diekspor pada periode tersebut.');
|
||||
|
|
@ -111,11 +137,14 @@ public function exportExcel(Request $request)
|
|||
|
||||
public function exportPdf(Request $request)
|
||||
{
|
||||
$bulan = $request->input('bulan', date('m'));
|
||||
$tahun = $request->input('tahun', date('Y'));
|
||||
$bulan = $request->input('bulan', date('m'));
|
||||
$tahun = $request->input('tahun', date('Y'));
|
||||
$divisiId = $request->input('id_divisi');
|
||||
|
||||
$rekap = $this->buildRekap($bulan, $tahun, $divisiId);
|
||||
$authUser = Auth::user();
|
||||
$kantorId = $authUser->isGlobalAdmin() ? null : $authUser->id_kantor;
|
||||
|
||||
$rekap = $this->buildRekap($bulan, $tahun, $divisiId, $kantorId);
|
||||
|
||||
$pdf = Pdf::loadView('laporan.pdf', compact('rekap', 'bulan', 'tahun'));
|
||||
$pdf->setPaper('a4', 'landscape');
|
||||
|
|
@ -123,19 +152,24 @@ public function exportPdf(Request $request)
|
|||
return $pdf->download("Laporan_Presensi_{$bulan}_{$tahun}.pdf");
|
||||
}
|
||||
|
||||
private function buildRekap(int $bulan, int $tahun, $divisiId = null): array
|
||||
private function buildRekap(int $bulan, int $tahun, $divisiId = null, $kantorId = null): array
|
||||
{
|
||||
$idTepatWaktu = StatusPresensi::TEPAT_WAKTU;
|
||||
$idTerlambat = StatusPresensi::TERLAMBAT;
|
||||
$idIzin = StatusPresensi::IZIN;
|
||||
$idSakit = StatusPresensi::SAKIT;
|
||||
$idAlpha = StatusPresensi::ALPHA;
|
||||
$idTerlambat = StatusPresensi::TERLAMBAT;
|
||||
$idIzin = StatusPresensi::IZIN;
|
||||
$idSakit = StatusPresensi::SAKIT;
|
||||
$idAlpha = StatusPresensi::ALPHA;
|
||||
|
||||
$pegawaiQuery = User::with(['jabatan', 'divisi'])
|
||||
->where('status_aktif', 1)
|
||||
->bukanHrd()
|
||||
->select('id', 'nama_lengkap', 'nik', 'id_jabatan', 'id_divisi')
|
||||
->orderBy('nama_lengkap', 'asc');
|
||||
|
||||
if ($kantorId) {
|
||||
$pegawaiQuery->where('id_kantor', $kantorId);
|
||||
}
|
||||
|
||||
if ($divisiId) {
|
||||
$pegawaiQuery->where('id_divisi', $divisiId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Enums\StatusPengajuan;
|
||||
use App\Models\User;
|
||||
use App\Models\Lembur;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
|
|
@ -15,83 +17,80 @@ class LaporanLemburController extends Controller
|
|||
|
||||
public function index(Request $request)
|
||||
{
|
||||
$bulan = $request->input('bulan', date('m'));
|
||||
$tahun = $request->input('tahun', date('Y'));
|
||||
$search = $request->input('search');
|
||||
$bulan = $request->input('bulan', date('m'));
|
||||
$tahun = $request->input('tahun', date('Y'));
|
||||
$search = $request->input('search');
|
||||
$divisiId = $request->input('id_divisi');
|
||||
|
||||
$query = User::with(['divisi', 'jabatan', 'lemburs' => function($q) use ($bulan, $tahun) {
|
||||
$q->whereYear('tanggal_lembur', $tahun)
|
||||
->whereMonth('tanggal_lembur', $bulan)
|
||||
->where('id_status', 2);
|
||||
}])
|
||||
->whereHas('roles', function($q) {
|
||||
$q->where('nama_role', '!=', 'super_admin');
|
||||
});
|
||||
$authUser = Auth::user();
|
||||
$isGlobalAdmin = $authUser->isGlobalAdmin();
|
||||
|
||||
if ($search) {
|
||||
$query->where(function($q) use ($search) {
|
||||
$q->where('nama_lengkap', 'like', '%' . $search . '%')
|
||||
->orWhere('nik', 'like', '%' . $search . '%');
|
||||
});
|
||||
}
|
||||
[$pegawai, $rekap] = $this->buildRekap(
|
||||
$bulan, $tahun, $divisiId,
|
||||
$isGlobalAdmin ? null : $authUser->id_kantor,
|
||||
$search
|
||||
);
|
||||
|
||||
if ($divisiId) {
|
||||
$query->where('id_divisi', $divisiId);
|
||||
}
|
||||
|
||||
$pegawai = $query->orderBy('nama_lengkap', 'asc')->paginate(15)->withQueryString();
|
||||
$divisiList = \App\Models\Divisi::all();
|
||||
|
||||
$rekap = [];
|
||||
foreach ($pegawai as $p) {
|
||||
$totalMenit = $p->lemburs->sum('durasi_menit');
|
||||
$jam = floor($totalMenit / 60);
|
||||
$menit = $totalMenit % 60;
|
||||
|
||||
$rekap[$p->id] = [
|
||||
'total_menit' => $totalMenit,
|
||||
'format_jam' => "{$jam}j {$menit}m",
|
||||
'jumlah_hari' => $p->lemburs->count(),
|
||||
'poin_diperoleh' => $p->lemburs->sum('jumlah_poin'),
|
||||
];
|
||||
}
|
||||
|
||||
return view('laporan-lembur.index', compact('pegawai', 'rekap', 'bulan', 'tahun', 'search', 'divisiId', 'divisiList'));
|
||||
}
|
||||
|
||||
public function exportExcel(Request $request)
|
||||
{
|
||||
$bulan = $request->input('bulan', date('m'));
|
||||
$tahun = $request->input('tahun', date('Y'));
|
||||
$bulan = $request->input('bulan', date('m'));
|
||||
$tahun = $request->input('tahun', date('Y'));
|
||||
$divisiId = $request->input('id_divisi');
|
||||
|
||||
$query = User::with(['divisi', 'jabatan', 'lemburs' => function($q) use ($bulan, $tahun) {
|
||||
$q->whereYear('tanggal_lembur', $tahun)
|
||||
->whereMonth('tanggal_lembur', $bulan)
|
||||
->where('id_status', 2);
|
||||
}])
|
||||
->whereHas('roles', function($q) {
|
||||
$q->where('nama_role', '!=', 'super_admin');
|
||||
});
|
||||
$authUser = Auth::user();
|
||||
$isGlobalAdmin = $authUser->isGlobalAdmin();
|
||||
|
||||
if ($divisiId) {
|
||||
$query->where('id_divisi', $divisiId);
|
||||
}
|
||||
[$pegawai, $rekap] = $this->buildRekap(
|
||||
$bulan, $tahun, $divisiId,
|
||||
$isGlobalAdmin ? null : $authUser->id_kantor
|
||||
);
|
||||
|
||||
$pegawai = $query->orderBy('nama_lengkap', 'asc')->get();
|
||||
|
||||
// Guard: cek apakah ada data lembur di periode tersebut
|
||||
$adaDataLembur = $pegawai->contains(fn($p) => $p->lemburs->isNotEmpty());
|
||||
if (!$adaDataLembur) {
|
||||
return redirect()->back()->with('error', 'Tidak ada data lembur yang disetujui untuk diekspor pada periode tersebut.');
|
||||
}
|
||||
|
||||
$filename = "Laporan_Lembur_{$bulan}_{$tahun}.xlsx";
|
||||
|
||||
return Excel::download(new LaporanLemburExport($pegawai, $rekap, $bulan, $tahun), $filename);
|
||||
}
|
||||
|
||||
private function buildRekap(string $bulan, string $tahun, $divisiId = null, $kantorId = null, $search = null): array
|
||||
{
|
||||
$query = User::with(['divisi', 'jabatan', 'lemburs' => function ($q) use ($bulan, $tahun) {
|
||||
$q->whereYear('tanggal_lembur', $tahun)
|
||||
->whereMonth('tanggal_lembur', $bulan)
|
||||
->where('id_status', StatusPengajuan::DISETUJUI);
|
||||
}])
|
||||
->bukanHrd();
|
||||
|
||||
if ($kantorId) {
|
||||
$query->where('id_kantor', $kantorId);
|
||||
}
|
||||
|
||||
if ($divisiId) {
|
||||
$query->where('id_divisi', $divisiId);
|
||||
}
|
||||
|
||||
if ($search) {
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('nama_lengkap', 'like', '%' . $search . '%')
|
||||
->orWhere('nik', 'like', '%' . $search . '%');
|
||||
});
|
||||
}
|
||||
|
||||
$pegawai = $query->orderBy('nama_lengkap', 'asc')->paginate(15)->withQueryString();
|
||||
|
||||
$rekap = [];
|
||||
foreach ($pegawai as $p) {
|
||||
$totalMenit = $p->lemburs->sum('durasi_menit');
|
||||
$jam = floor($totalMenit / 60);
|
||||
$menit = $totalMenit % 60;
|
||||
$jam = floor($totalMenit / 60);
|
||||
$menit = $totalMenit % 60;
|
||||
|
||||
$rekap[$p->id] = [
|
||||
'total_menit' => $totalMenit,
|
||||
|
|
@ -101,8 +100,6 @@ public function exportExcel(Request $request)
|
|||
];
|
||||
}
|
||||
|
||||
$filename = "Laporan_Lembur_{$bulan}_{$tahun}.xlsx";
|
||||
|
||||
return Excel::download(new LaporanLemburExport($pegawai, $rekap, $bulan, $tahun), $filename);
|
||||
return [$pegawai, $rekap];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
use App\Services\LemburService;
|
||||
use App\Services\NotifikasiService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class LemburController extends Controller
|
||||
|
|
@ -23,10 +24,10 @@ public function __construct(LemburService $lemburService)
|
|||
|
||||
public function index()
|
||||
{
|
||||
$user = \Illuminate\Support\Facades\Auth::user();
|
||||
$user = Auth::user();
|
||||
$isGlobalAdmin = $user->isGlobalAdmin();
|
||||
|
||||
$query = Lembur::with(['user', 'status', 'kompensasi']);
|
||||
$query = Lembur::with(['user.jabatan', 'status', 'kompensasi']);
|
||||
|
||||
if (!$isGlobalAdmin) {
|
||||
$query->whereHas('user', function ($q) use ($user) {
|
||||
|
|
@ -43,7 +44,14 @@ public function index()
|
|||
|
||||
public function create()
|
||||
{
|
||||
$pegawai = User::where('status_aktif', 1)->orderBy('nama_lengkap', 'asc')->get();
|
||||
$user = Auth::user();
|
||||
$query = User::where('status_aktif', 1);
|
||||
|
||||
if (!$user->isGlobalAdmin()) {
|
||||
$query->where('id_kantor', $user->id_kantor);
|
||||
}
|
||||
|
||||
$pegawai = $query->orderBy('nama_lengkap', 'asc')->get();
|
||||
$kompensasi = JenisKompensasi::all();
|
||||
return view('lembur.create', compact('pegawai', 'kompensasi'));
|
||||
}
|
||||
|
|
@ -53,19 +61,40 @@ public function store(Request $request)
|
|||
$request->validate([
|
||||
'id_user' => 'required|exists:users,id',
|
||||
'tanggal_lembur' => 'required|date',
|
||||
'jam_mulai' => 'required',
|
||||
'jam_selesai' => 'required',
|
||||
'jam_mulai' => 'required|date_format:H:i',
|
||||
'jam_selesai' => 'required|date_format:H:i|different:jam_mulai',
|
||||
'id_kompensasi' => 'required|exists:jenis_kompensasi,id_kompensasi',
|
||||
'keterangan' => 'nullable|string'
|
||||
'keterangan' => 'nullable|string|max:255'
|
||||
]);
|
||||
|
||||
$userAuth = Auth::user();
|
||||
$targetUser = User::findOrFail($request->id_user);
|
||||
|
||||
if (!$userAuth->isGlobalAdmin() && $targetUser->id_kantor != $userAuth->id_kantor) {
|
||||
return redirect()->back()->with('error', 'Anda tidak diizinkan membuat lembur untuk pegawai kantor lain.')->withInput();
|
||||
}
|
||||
|
||||
try {
|
||||
DB::transaction(function () use ($request) {
|
||||
$user = User::findOrFail($request->id_user);
|
||||
$lembur = $this->lemburService->createLembur($user, $request->all());
|
||||
$lembur = null;
|
||||
DB::transaction(function () use ($request, $targetUser, &$lembur) {
|
||||
$lembur = $this->lemburService->createLembur($targetUser, $request->all());
|
||||
$this->lemburService->approve($lembur);
|
||||
});
|
||||
|
||||
if ($lembur) {
|
||||
$message = 'Lembur manual Anda pada tanggal ' . $lembur->tanggal_lembur->translatedFormat('d F Y') . ' telah ditambahkan dan disetujui oleh HRD.';
|
||||
if ($lembur->id_kompensasi == JenisKompensasiEnum::TAMBAHAN_POIN) {
|
||||
$message .= ' Poin telah ditambahkan ke saldo Anda.';
|
||||
}
|
||||
app(NotifikasiService::class)->kirim(
|
||||
$targetUser->id,
|
||||
'lembur_disetujui',
|
||||
'Lembur Manual Disetujui',
|
||||
$message,
|
||||
['id_lembur' => $lembur->id_lembur]
|
||||
);
|
||||
}
|
||||
|
||||
return redirect()->route('lembur.index')->with('success', 'Data lembur manual berhasil ditambahkan dan disetujui otomatis.');
|
||||
} catch (\Exception $e) {
|
||||
return redirect()->back()->with('error', 'Gagal menambahkan lembur: ' . $e->getMessage())->withInput();
|
||||
|
|
@ -76,11 +105,11 @@ public function update(Request $request, Lembur $lembur)
|
|||
{
|
||||
$request->validate([
|
||||
'action' => 'required|in:approve,reject',
|
||||
'alasan_penolakan' => 'required_if:action,reject|nullable|string'
|
||||
'alasan_penolakan' => 'required_if:action,reject|nullable|string|max:500'
|
||||
]);
|
||||
|
||||
$lembur->load('user');
|
||||
$userAuth = \Illuminate\Support\Facades\Auth::user();
|
||||
$userAuth = Auth::user();
|
||||
if (!$userAuth->isGlobalAdmin() && $lembur->user->id_kantor != $userAuth->id_kantor) {
|
||||
return redirect()->back()->with('error', 'Anda tidak diizinkan memproses data dari kantor lain.');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,10 +17,7 @@ class PegawaiController extends Controller
|
|||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$query = User::with(['divisi', 'jabatan', 'kantor', 'roles'])
|
||||
->whereDoesntHave('roles', function ($q) {
|
||||
$q->where('nama_role', 'hrd');
|
||||
});
|
||||
$query = User::with(['divisi', 'jabatan', 'kantor', 'roles'])->bukanHrd();
|
||||
|
||||
if ($request->filled('filter_jabatan')) {
|
||||
$query->where('id_jabatan', $request->filter_jabatan);
|
||||
|
|
@ -48,21 +45,14 @@ public function index(Request $request)
|
|||
|
||||
$allKantor = Kantor::withCount([
|
||||
'users as total_pegawai' => function ($q) {
|
||||
$q->whereDoesntHave('roles', function ($sq) {
|
||||
$sq->where('nama_role', 'hrd');
|
||||
});
|
||||
$q->bukanHrd();
|
||||
}
|
||||
])->get();
|
||||
|
||||
$stats = [
|
||||
'kantor_list' => $allKantor,
|
||||
'total' => User::whereDoesntHave('roles', function ($q) {
|
||||
$q->where('nama_role', 'hrd');
|
||||
})->count(),
|
||||
'active' => User::where('status_aktif', 1)
|
||||
->whereDoesntHave('roles', function ($q) {
|
||||
$q->where('nama_role', 'hrd');
|
||||
})->count(),
|
||||
'total' => User::bukanHrd()->count(),
|
||||
'active' => User::bukanHrd()->where('status_aktif', 1)->count(),
|
||||
];
|
||||
|
||||
$allJabatan = Jabatan::all();
|
||||
|
|
|
|||
|
|
@ -7,8 +7,13 @@
|
|||
use App\Enums\StatusPresensi;
|
||||
use App\Enums\StatusSurat;
|
||||
use App\Enums\StatusValidasi;
|
||||
use App\Http\Requests\StorePengajuanIzinRequest;
|
||||
use App\Models\Divisi;
|
||||
use App\Models\JenisIzin;
|
||||
use App\Models\Kantor;
|
||||
use App\Models\PengajuanIzin;
|
||||
use App\Models\Presensi;
|
||||
use App\Models\StatusPengajuan as StatusPengajuanModel;
|
||||
use App\Models\User;
|
||||
use App\Models\SuratIzin;
|
||||
use App\Models\TandaTangan;
|
||||
|
|
@ -47,28 +52,31 @@ public function index(Request $request)
|
|||
|
||||
$izin = $query->orderBy('created_at', 'desc')->paginate(15);
|
||||
|
||||
$statuses = \App\Models\StatusPengajuan::all();
|
||||
$jenisIzin = \App\Models\JenisIzin::all();
|
||||
$users = \App\Models\User::with(['kantor', 'divisi', 'jabatan'])->select('id', 'nama_lengkap', 'nik', 'id_kantor', 'id_divisi', 'id_jabatan')->orderBy('nama_lengkap', 'asc')->get();
|
||||
$kantor = \App\Models\Kantor::all();
|
||||
$divisi = \App\Models\Divisi::all();
|
||||
$statuses = StatusPengajuanModel::orderBy('id_status')->get();
|
||||
$jenisIzinList = JenisIzin::all();
|
||||
|
||||
return view('izin.index', compact('izin', 'statusId', 'statuses', 'jenisIzin', 'users', 'kantor', 'divisi'));
|
||||
$usersQuery = User::with(['kantor', 'divisi', 'jabatan'])
|
||||
->select('id', 'nama_lengkap', 'nik', 'id_kantor', 'id_divisi', 'id_jabatan')
|
||||
->where('status_aktif', 1)
|
||||
->bukanHrd()
|
||||
->orderBy('nama_lengkap', 'asc');
|
||||
|
||||
if (!$isGlobalAdmin) {
|
||||
$usersQuery->where('id_kantor', $user->id_kantor);
|
||||
}
|
||||
|
||||
$users = $usersQuery->get();
|
||||
$kantor = $isGlobalAdmin ? Kantor::all() : Kantor::where('id_kantor', $user->id_kantor)->get();
|
||||
$divisi = Divisi::all();
|
||||
|
||||
return view('izin.index', compact('izin', 'statusId', 'statuses', 'jenisIzinList', 'users', 'kantor', 'divisi'));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
public function store(StorePengajuanIzinRequest $request)
|
||||
{
|
||||
$request->validate([
|
||||
'id_user' => 'required|exists:users,id',
|
||||
'id_jenis_izin' => 'required|exists:jenis_izin,id_jenis_izin',
|
||||
'tanggal_mulai' => 'required|date|after_or_equal:today',
|
||||
'tanggal_selesai' => 'required|date|after_or_equal:tanggal_mulai',
|
||||
'alasan' => 'required|string',
|
||||
'bukti_file' => 'nullable|file|mimes:jpeg,png,jpg,pdf|max:2048'
|
||||
]);
|
||||
|
||||
$jenisIzin = \App\Models\JenisIzin::find($request->id_jenis_izin);
|
||||
if ($jenisIzin && $jenisIzin->nama_izin == 'Cuti') {
|
||||
$jenisIzinData = JenisIzin::find($request->id_jenis_izin);
|
||||
if ($jenisIzinData && $jenisIzinData->id_jenis_izin == JenisIzinEnum::CUTI) {
|
||||
$pegawai = User::find($request->id_user);
|
||||
$tanggalMulai = Carbon::parse($request->tanggal_mulai);
|
||||
$tanggalSelesai = Carbon::parse($request->tanggal_selesai);
|
||||
|
|
@ -88,12 +96,12 @@ public function store(Request $request)
|
|||
}
|
||||
}
|
||||
|
||||
if ($jenisIzin && $jenisIzin->id_jenis_izin == \App\Enums\JenisIzin::SAKIT) {
|
||||
if ($jenisIzinData && $jenisIzinData->id_jenis_izin == JenisIzinEnum::SAKIT) {
|
||||
$tanggalMulai = Carbon::parse($request->tanggal_mulai);
|
||||
$jumlahHari = $tanggalMulai->diffInDays(Carbon::parse($request->tanggal_selesai)) + 1;
|
||||
|
||||
$sudahSakitBulanIni = PengajuanIzin::where('id_user', $request->id_user)
|
||||
->where('id_jenis_izin', \App\Enums\JenisIzin::SAKIT)
|
||||
->where('id_jenis_izin', JenisIzinEnum::SAKIT)
|
||||
->whereIn('id_status', [StatusPengajuan::PENDING, StatusPengajuan::DISETUJUI])
|
||||
->whereMonth('tanggal_mulai', $tanggalMulai->month)
|
||||
->whereYear('tanggal_mulai', $tanggalMulai->year)
|
||||
|
|
@ -127,7 +135,7 @@ public function store(Request $request)
|
|||
return back()->withInput()->with('error', 'Pegawai sudah memiliki pengajuan Izin/Cuti (Pending/Disetujui) pada rentang tanggal tersebut!');
|
||||
}
|
||||
|
||||
$existingPresensi = \App\Models\Presensi::where('id_user', $userId)
|
||||
$existingPresensi = Presensi::where('id_user', $userId)
|
||||
->whereBetween('tanggal', [$tglMulai, $tglSelesai])
|
||||
->whereNotNull('jam_masuk')
|
||||
->exists();
|
||||
|
|
@ -260,10 +268,9 @@ public function approve($id)
|
|||
|
||||
public function reject(Request $request, $id)
|
||||
{
|
||||
$izin = PengajuanIzin::with('user')->findOrFail($id);
|
||||
$izin = PengajuanIzin::with(['user', 'jenisIzin'])->findOrFail($id);
|
||||
$user = Auth::user();
|
||||
|
||||
// Security Check
|
||||
if (!$user->isGlobalAdmin() && $izin->user->id_kantor != $user->id_kantor) {
|
||||
return redirect()->back()->with('error', 'Anda tidak diizinkan menolak pengajuan dari kantor lain.');
|
||||
}
|
||||
|
|
@ -276,18 +283,29 @@ public function reject(Request $request, $id)
|
|||
return redirect()->back()->with('error', 'Pengajuan Cuti harus ditolak melalui menu Surat Izin.');
|
||||
}
|
||||
|
||||
$izin->update([
|
||||
'id_status' => StatusPengajuan::DITOLAK
|
||||
]);
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
app(NotifikasiService::class)->kirim(
|
||||
$izin->id_user,
|
||||
'izin_ditolak',
|
||||
'Pengajuan Izin Ditolak',
|
||||
'Pengajuan ' . ($izin->jenisIzin->nama_izin ?? 'Izin') . ' Anda ditolak.',
|
||||
['id_izin' => $izin->id_izin]
|
||||
);
|
||||
$izin->update([
|
||||
'id_status' => StatusPengajuan::DITOLAK,
|
||||
'alasan_penolakan' => $request->alasan_penolakan,
|
||||
]);
|
||||
|
||||
return redirect()->back()->with('success', 'Pengajuan izin berhasil ditolak.');
|
||||
DB::commit();
|
||||
|
||||
app(NotifikasiService::class)->kirim(
|
||||
$izin->id_user,
|
||||
'izin_ditolak',
|
||||
'Pengajuan Izin Ditolak',
|
||||
'Pengajuan ' . ($izin->jenisIzin->nama_izin ?? 'Izin') . ' Anda ditolak. Catatan: ' . ($request->alasan_penolakan ?? '-'),
|
||||
['id_izin' => $izin->id_izin]
|
||||
);
|
||||
|
||||
return redirect()->back()->with('success', 'Pengajuan izin berhasil ditolak.');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
return redirect()->back()->with('error', 'Gagal menolak pengajuan: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
use App\Services\PoinService;
|
||||
use App\Services\NotifikasiService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class PenggunaanPoinController extends Controller
|
||||
|
|
@ -20,10 +21,10 @@ public function __construct(PoinService $poinService)
|
|||
|
||||
public function index()
|
||||
{
|
||||
$user = \Illuminate\Support\Facades\Auth::user();
|
||||
$user = Auth::user();
|
||||
$isGlobalAdmin = $user->isGlobalAdmin();
|
||||
|
||||
$query = PenggunaanPoin::with(['user', 'jenisPengurangan', 'status']);
|
||||
$query = PenggunaanPoin::with(['user.divisi', 'jenisPengurangan', 'status']);
|
||||
|
||||
if (!$isGlobalAdmin) {
|
||||
$query->whereHas('user', function ($q) use ($user) {
|
||||
|
|
@ -48,7 +49,7 @@ public function update(Request $request, $id)
|
|||
$penggunaan = PenggunaanPoin::findOrFail($id);
|
||||
|
||||
$penggunaan->load('user');
|
||||
$userAuth = \Illuminate\Support\Facades\Auth::user();
|
||||
$userAuth = Auth::user();
|
||||
if (!$userAuth->isGlobalAdmin() && $penggunaan->user->id_kantor != $userAuth->id_kantor) {
|
||||
return redirect()->back()->with('error', 'Anda tidak diizinkan memproses data dari kantor lain.');
|
||||
}
|
||||
|
|
@ -83,7 +84,7 @@ public function update(Request $request, $id)
|
|||
$penggunaan->id_user,
|
||||
'poin_disetujui',
|
||||
'Penggunaan Poin Disetujui',
|
||||
'Pengajuan penggunaan poin Anda pada tanggal ' . $penggunaan->tanggal_penggunaan . ' telah disetujui.',
|
||||
'Pengajuan penggunaan poin Anda pada tanggal ' . ($penggunaan->tanggal_penggunaan ? $penggunaan->tanggal_penggunaan->translatedFormat('d F Y') : '-') . ' telah disetujui.',
|
||||
['id_penggunaan' => $penggunaan->id_penggunaan]
|
||||
);
|
||||
|
||||
|
|
@ -103,7 +104,7 @@ public function update(Request $request, $id)
|
|||
$penggunaan->id_user,
|
||||
'poin_ditolak',
|
||||
'Penggunaan Poin Ditolak',
|
||||
'Pengajuan penggunaan poin Anda pada tanggal ' . $penggunaan->tanggal_penggunaan . ' ditolak.',
|
||||
'Pengajuan penggunaan poin Anda pada tanggal ' . ($penggunaan->tanggal_penggunaan ? $penggunaan->tanggal_penggunaan->translatedFormat('d F Y') : '-') . ' ditolak. Alasan: ' . ($request->alasan_penolakan ?? '-'),
|
||||
['id_penggunaan' => $penggunaan->id_penggunaan]
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ public function store(StorePengumumanRequest $request)
|
|||
'pengumuman_baru',
|
||||
'📢 Pengumuman Baru',
|
||||
$pengumuman->judul,
|
||||
['id_pengumuman' => $pengumuman->id]
|
||||
['id_pengumuman' => $pengumuman->id_pengumuman]
|
||||
);
|
||||
|
||||
return redirect()->route('pengumuman.index')
|
||||
|
|
@ -86,16 +86,20 @@ public function update(UpdatePengumumanRequest $request, Pengumuman $pengumuman)
|
|||
|
||||
public function destroy(Pengumuman $pengumuman)
|
||||
{
|
||||
if ($pengumuman->foto) {
|
||||
Storage::disk('public')->delete($pengumuman->foto);
|
||||
}
|
||||
if ($pengumuman->lampiran) {
|
||||
Storage::disk('public')->delete($pengumuman->lampiran);
|
||||
}
|
||||
try {
|
||||
if ($pengumuman->foto) {
|
||||
Storage::disk('public')->delete($pengumuman->foto);
|
||||
}
|
||||
if ($pengumuman->lampiran) {
|
||||
Storage::disk('public')->delete($pengumuman->lampiran);
|
||||
}
|
||||
|
||||
$pengumuman->delete();
|
||||
$pengumuman->delete();
|
||||
|
||||
return redirect()->route('pengumuman.index')
|
||||
->with('success', 'Pengumuman berhasil dihapus.');
|
||||
return redirect()->route('pengumuman.index')
|
||||
->with('success', 'Pengumuman berhasil dihapus.');
|
||||
} catch (\Exception $e) {
|
||||
return redirect()->back()->with('error', 'Gagal menghapus pengumuman: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@
|
|||
use App\Http\Requests\StoreManualPresensiRequest;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
|
|
@ -228,6 +229,7 @@ public function create()
|
|||
$isGlobalAdmin = $user->isGlobalAdmin();
|
||||
|
||||
$pegawaiQuery = \App\Models\User::with('divisi')
|
||||
->bukanHrd()
|
||||
->orderBy('nama_lengkap', 'asc');
|
||||
|
||||
if (!$isGlobalAdmin) {
|
||||
|
|
@ -312,7 +314,7 @@ public function updateManual(Request $request, $id)
|
|||
'alasan_telat' => 'required|string|max:255'
|
||||
], [
|
||||
'alasan_telat.required' => 'Catatan koreksi wajib diisi sebagai dokumentasi log.',
|
||||
'jam_masuk.required' => 'Jam masuk wajib diisi untuk kehadiran Hadir/Terlambat.'
|
||||
'jam_masuk.required' => 'Jam masuk wajib diisi untuk kehadiran Hadir/Terlambat.'
|
||||
]);
|
||||
|
||||
$presensi = Presensi::findOrFail($id);
|
||||
|
|
@ -425,7 +427,7 @@ public function store(StorePresensiRequest $request)
|
|||
$faceMessage = null;
|
||||
|
||||
try {
|
||||
$faceService = new \App\Services\FaceRecognitionService();
|
||||
$faceService = app(\App\Services\FaceRecognitionService::class);
|
||||
$result = $faceService->verifyFace($user->id, $fotoFile);
|
||||
|
||||
if (isset($result['verified']) && $result['verified'] === true) {
|
||||
|
|
@ -434,6 +436,10 @@ public function store(StorePresensiRequest $request)
|
|||
|
||||
$faceMessage = $result['message'] ?? null;
|
||||
} catch (\Exception $e) {
|
||||
Log::warning('Verifikasi wajah masuk gagal', [
|
||||
'user_id' => $user->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
$fotoFile->storeAs('uploads/absensi', $imageName, 'public');
|
||||
|
|
@ -486,12 +492,16 @@ private function handlePulang($request, $user, $hariIni, $jamSekarang)
|
|||
|
||||
$verifikasiPulang = 0;
|
||||
try {
|
||||
$faceService = new \App\Services\FaceRecognitionService();
|
||||
$faceService = app(\App\Services\FaceRecognitionService::class);
|
||||
$result = $faceService->verifyFace($user->id, $fotoFile);
|
||||
if (isset($result['verified']) && $result['verified'] === true) {
|
||||
$verifikasiPulang = 1;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::warning('Verifikasi wajah pulang gagal', [
|
||||
'user_id' => $user->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
$fotoFile->storeAs('uploads/absensi', $imageName, 'public');
|
||||
|
|
|
|||
|
|
@ -2,34 +2,25 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Requests\UpdateProfileRequest;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class ProfileController extends Controller
|
||||
{
|
||||
public function edit()
|
||||
{
|
||||
$user = Auth::user();
|
||||
return view('profile.edit', compact('user'));
|
||||
return view('profile.edit');
|
||||
}
|
||||
|
||||
public function update(Request $request)
|
||||
public function update(UpdateProfileRequest $request)
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
$request->validate([
|
||||
'nama_lengkap' => 'required|string|max:255',
|
||||
'email' => ['required', 'string', 'email', 'max:255', Rule::unique('users')->ignore($user->id)],
|
||||
'foto' => 'nullable|image|mimes:jpeg,png,jpg,gif|max:2048',
|
||||
'password' => 'nullable|string|min:8|confirmed',
|
||||
]);
|
||||
|
||||
$data = [
|
||||
'nama_lengkap' => $request->nama_lengkap,
|
||||
'email' => $request->email,
|
||||
'email' => $request->email,
|
||||
];
|
||||
|
||||
if ($request->filled('password')) {
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@ public function approve(Request $request, $id)
|
|||
public function reject(Request $request, $id)
|
||||
{
|
||||
try {
|
||||
$surat = SuratIzin::with('user')->findOrFail($id);
|
||||
$surat = SuratIzin::with(['user', 'pengajuanIzin'])->findOrFail($id);
|
||||
$user = Auth::user();
|
||||
$isGlobalAdmin = $user->isGlobalAdmin();
|
||||
|
||||
|
|
|
|||
|
|
@ -18,18 +18,26 @@ class TukarShiftController extends Controller
|
|||
|
||||
public function index()
|
||||
{
|
||||
$riwayat = RiwayatTukarShift::with(['user1', 'user2', 'jadwal1.shift', 'jadwal2.shift', 'execAdmin'])
|
||||
->latest()
|
||||
->get();
|
||||
$user = Auth::user();
|
||||
|
||||
$query = RiwayatTukarShift::with(['user1', 'user2', 'jadwal1.shift', 'jadwal2.shift', 'execAdmin'])
|
||||
->latest();
|
||||
|
||||
if (!$user->isGlobalAdmin()) {
|
||||
$query->where(function ($q) use ($user) {
|
||||
$q->whereHas('user1', fn($u) => $u->where('id_kantor', $user->id_kantor))
|
||||
->orWhereHas('user2', fn($u) => $u->where('id_kantor', $user->id_kantor));
|
||||
});
|
||||
}
|
||||
|
||||
$riwayat = $query->get();
|
||||
|
||||
return view('tukar-shift.index', compact('riwayat'));
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
$pegawai = User::with('kantor')->whereDoesntHave('roles', function ($q) {
|
||||
$q->where('nama_role', 'hrd');
|
||||
})->get();
|
||||
$pegawai = User::with('kantor')->bukanHrd()->get();
|
||||
|
||||
$kantor = \App\Models\Kantor::orderBy('nama_kantor', 'asc')->get();
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class BulkDestroyJadwalRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'tanggal_mulai' => ['required', 'date'],
|
||||
'tanggal_selesai' => ['required', 'date', 'after_or_equal:tanggal_mulai'],
|
||||
'user_ids' => ['required', 'array', 'min:1'],
|
||||
'user_ids.*' => ['exists:users,id'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'tanggal_mulai.required' => 'Tanggal mulai wajib diisi.',
|
||||
'tanggal_selesai.required' => 'Tanggal selesai wajib diisi.',
|
||||
'tanggal_selesai.after_or_equal' => 'Tanggal selesai tidak boleh sebelum tanggal mulai.',
|
||||
'user_ids.required' => 'Pilih minimal 1 pegawai.',
|
||||
'user_ids.min' => 'Pilih minimal 1 pegawai.',
|
||||
'user_ids.*.exists' => 'Salah satu pegawai yang dipilih tidak valid.',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -9,18 +9,15 @@ class StoreHariLiburRequest extends FormRequest
|
|||
|
||||
public function authorize(): bool
|
||||
{
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
|
||||
'tanggal' => ['required', 'date'],
|
||||
'keterangan' => ['required', 'string', 'max:255'],
|
||||
'id_kantor' => ['nullable', 'exists:kantor,id_kantor'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreJadwalRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'tanggal_mulai' => ['required', 'date'],
|
||||
'tanggal_selesai' => ['required', 'date', 'after_or_equal:tanggal_mulai'],
|
||||
'id_shift' => ['required', 'exists:shift_kerja,id_shift'],
|
||||
'user_ids' => ['required', 'array', 'min:1'],
|
||||
'user_ids.*' => ['exists:users,id'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'tanggal_mulai.required' => 'Tanggal mulai wajib diisi.',
|
||||
'tanggal_selesai.required' => 'Tanggal selesai wajib diisi.',
|
||||
'tanggal_selesai.after_or_equal' => 'Tanggal selesai tidak boleh sebelum tanggal mulai.',
|
||||
'id_shift.required' => 'Shift kerja wajib dipilih.',
|
||||
'id_shift.exists' => 'Shift yang dipilih tidak valid.',
|
||||
'user_ids.required' => 'Pilih minimal 1 pegawai.',
|
||||
'user_ids.min' => 'Pilih minimal 1 pegawai.',
|
||||
'user_ids.*.exists' => 'Salah satu pegawai yang dipilih tidak valid.',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -17,7 +17,8 @@ public function rules(): array
|
|||
'nama_lengkap' => ['required', 'string', 'max:255'],
|
||||
'nik' => ['nullable', 'string', 'max:50', 'unique:users,nik'],
|
||||
'email' => ['required', 'email', 'unique:users,email'],
|
||||
'no_telp' => ['nullable', 'string', 'max:20'],
|
||||
'no_telp' =>
|
||||
['nullable', 'string', 'max:15', 'regex:/^(\+62|62|0)[0-9]{8,11}$/'],
|
||||
'alamat' => ['nullable', 'string'],
|
||||
'id_divisi' => ['required', 'exists:divisi,id_divisi'],
|
||||
'id_jabatan' => ['required', 'exists:jabatan,id_jabatan'],
|
||||
|
|
@ -32,10 +33,13 @@ public function rules(): array
|
|||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'nik.unique' => 'NIK sudah terdaftar di sistem.',
|
||||
'email.unique' => 'Email ini sudah digunakan oleh pegawai lain.',
|
||||
'nik.unique' => 'NIK sudah terdaftar di sistem.',
|
||||
'email.unique' => 'Email ini sudah digunakan oleh pegawai lain.',
|
||||
'id_divisi.exists' => 'Divisi yang dipilih tidak valid.',
|
||||
'id_role.exists' => 'Role yang dipilih tidak valid.',
|
||||
'id_role.exists' => 'Role yang dipilih tidak valid.',
|
||||
'no_telp.regex' => 'Format nomor telepon tidak valid. Gunakan format: 08xx, +62xx, atau 62xx.',
|
||||
'foto.max' => 'Ukuran foto tidak boleh lebih dari 5MB.',
|
||||
'sisa_cuti.min' => 'Sisa cuti tidak boleh negatif.',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -20,11 +20,12 @@ public function authorize(): bool
|
|||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'tanggal_mulai' => 'required|date|after_or_equal:today',
|
||||
'tanggal_selesai' => 'required|date|after_or_equal:tanggal_mulai',
|
||||
'id_jenis_izin' => 'required|exists:jenis_izin,id_jenis_izin',
|
||||
'alasan' => 'required|string|max:150',
|
||||
'bukti_file' => 'nullable|file|mimes:pdf,jpg,jpeg,png|max:2048',
|
||||
'id_user' => 'required|exists:users,id',
|
||||
'id_jenis_izin' => 'required|exists:jenis_izin,id_jenis_izin',
|
||||
'tanggal_mulai' => 'required|date',
|
||||
'tanggal_selesai'=> 'required|date|after_or_equal:tanggal_mulai',
|
||||
'alasan' => 'required|string|max:500',
|
||||
'bukti_file' => 'nullable|file|mimes:pdf,jpg,jpeg,png|max:2048',
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -36,10 +37,12 @@ public function rules(): array
|
|||
public function attributes()
|
||||
{
|
||||
return [
|
||||
'tanggal_mulai' => 'tanggal mulai',
|
||||
'id_user' => 'pegawai',
|
||||
'id_jenis_izin' => 'jenis izin',
|
||||
'tanggal_mulai' => 'tanggal mulai',
|
||||
'tanggal_selesai' => 'tanggal selesai',
|
||||
'id_jenis_izin' => 'jenis izin',
|
||||
'bukti_file' => 'bukti file',
|
||||
'alasan' => 'alasan',
|
||||
'bukti_file' => 'bukti file',
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,18 +22,34 @@ public function rules(): array
|
|||
'email' => [
|
||||
'required',
|
||||
'email',
|
||||
'unique:users,email,' . $this->route('pegawai')
|
||||
Rule::unique('users', 'email')->ignore($id),
|
||||
],
|
||||
'no_telp' => ['nullable', 'string', 'max:20'],
|
||||
'alamat' => ['nullable', 'string'],
|
||||
'id_divisi' => ['required', 'exists:divisi,id_divisi'],
|
||||
'id_jabatan' => ['required', 'exists:jabatan,id_jabatan'],
|
||||
'id_kantor' => ['required', 'exists:kantor,id_kantor'],
|
||||
'no_telp' => ['nullable', 'string', 'max:15', 'regex:/^(\+62|62|0)[0-9]{8,11}$/'],
|
||||
'alamat' => ['nullable', 'string'],
|
||||
'id_divisi' => ['required', 'exists:divisi,id_divisi'],
|
||||
'id_jabatan' => ['required', 'exists:jabatan,id_jabatan'],
|
||||
'id_kantor' => ['required', 'exists:kantor,id_kantor'],
|
||||
'status_aktif' => ['required', 'in:0,1'],
|
||||
'tgl_bergabung' => ['required', 'date'],
|
||||
'foto' => ['nullable', 'image', 'max:5120'],
|
||||
'sisa_cuti' => ['nullable', 'integer', 'min:0'],
|
||||
'id_role' => ['required', 'exists:roles,id_role'],
|
||||
'foto' => ['nullable', 'image', 'max:5120'],
|
||||
'sisa_cuti' => ['nullable', 'integer', 'min:0'],
|
||||
'id_role' => ['required', 'exists:roles,id_role'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'nik.unique' => 'NIK sudah terdaftar di sistem.',
|
||||
'email.unique' => 'Email ini sudah digunakan oleh pegawai lain.',
|
||||
'id_divisi.exists' => 'Divisi yang dipilih tidak valid.',
|
||||
'id_jabatan.exists' => 'Jabatan yang dipilih tidak valid.',
|
||||
'id_kantor.exists' => 'Kantor yang dipilih tidak valid.',
|
||||
'id_role.exists' => 'Role yang dipilih tidak valid.',
|
||||
'status_aktif.in' => 'Status pegawai tidak valid.',
|
||||
'foto.max' => 'Ukuran foto tidak boleh lebih dari 5MB.',
|
||||
'sisa_cuti.min' => 'Sisa cuti tidak boleh negatif.',
|
||||
'no_telp.regex' => 'Format nomor telepon tidak valid. Gunakan format: 08xx, +62xx, atau 62xx.',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateProfileRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return Auth::check();
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'nama_lengkap' => ['required', 'string', 'max:255'],
|
||||
'email' => ['required', 'string', 'email', 'max:255', Rule::unique('users')->ignore(Auth::id())],
|
||||
'foto' => ['nullable', 'image', 'mimes:jpeg,png,jpg', 'max:2048'],
|
||||
'password' => ['nullable', 'string', 'min:8', 'confirmed'],
|
||||
];
|
||||
}
|
||||
|
||||
public function attributes(): array
|
||||
{
|
||||
return [
|
||||
'nama_lengkap' => 'Nama Lengkap',
|
||||
'email' => 'Alamat Email',
|
||||
'foto' => 'Foto Profil',
|
||||
'password' => 'Password Baru',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,9 @@
|
|||
use App\Enums\StatusVerifikasiWajah;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Queue\Queueable as QueueableTrait;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
|
@ -13,7 +15,7 @@
|
|||
|
||||
class RetrainAllModels implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public $timeout = 3600;
|
||||
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ protected static function booted()
|
|||
'tanggal_selesai',
|
||||
'alasan',
|
||||
'bukti_file',
|
||||
|
||||
'alasan_penolakan',
|
||||
];
|
||||
|
||||
public function user()
|
||||
|
|
|
|||
|
|
@ -156,4 +156,11 @@ public function notifikasi(): \Illuminate\Database\Eloquent\Relations\HasMany
|
|||
{
|
||||
return $this->hasMany(Notifikasi::class, 'id_user', 'id');
|
||||
}
|
||||
|
||||
public function scopeBukanHrd($query)
|
||||
{
|
||||
return $query->whereDoesntHave('roles', function ($q) {
|
||||
$q->where('nama_role', 'hrd');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -130,13 +130,12 @@ public function verifyFace($userId, UploadedFile $fotoMasuk)
|
|||
}
|
||||
|
||||
return [
|
||||
'verified' => ($output['match'] ?? false) === true,
|
||||
'confidence' => $output['confidence'] ?? null,
|
||||
'svm_confidence' => $output['svm_confidence'] ?? null,
|
||||
'normalized_distance' => $output['normalized_distance'] ?? null,
|
||||
'verified' => ($output['match'] ?? false) === true,
|
||||
'confidence' => $output['confidence'] ?? null,
|
||||
'svm_confidence' => $output['svm_confidence'] ?? null,
|
||||
'verification_status' => $output['verification_status'] ?? 'UNKNOWN',
|
||||
'blur_score' => $output['blur_score'] ?? null,
|
||||
'message' => $output['message'] ?? null,
|
||||
'blur_score' => $output['blur_score'] ?? null,
|
||||
'message' => $output['message'] ?? null,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,12 +48,6 @@ public function deductPoin($userId, $jumlahPoinDibutuhkan, $idPenggunaan)
|
|||
|
||||
$remainingNeeded -= $take;
|
||||
}
|
||||
|
||||
$penggunaan = PenggunaanPoin::find($idPenggunaan);
|
||||
if ($penggunaan) {
|
||||
$penggunaan->id_status = StatusPengajuan::DISETUJUI;
|
||||
$penggunaan->save();
|
||||
}
|
||||
}
|
||||
|
||||
public function refundPoin($idPenggunaan)
|
||||
|
|
@ -70,14 +64,9 @@ public function refundPoin($idPenggunaan)
|
|||
}
|
||||
$poinSumber->save();
|
||||
}
|
||||
$detail->delete();
|
||||
}
|
||||
|
||||
$penggunaan = PenggunaanPoin::find($idPenggunaan);
|
||||
if ($penggunaan) {
|
||||
$penggunaan->id_status = StatusPengajuan::DITOLAK;
|
||||
$penggunaan->save();
|
||||
}
|
||||
DetailPenggunaanPoin::where('id_penggunaan', $idPenggunaan)->delete();
|
||||
}
|
||||
|
||||
public function getActivePoints($userId)
|
||||
|
|
|
|||
|
|
@ -15,12 +15,12 @@ public function up(): void
|
|||
Schema::create('log_verifikasi_wajah', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('id_user');
|
||||
$table->decimal('confidence', 5, 4)->nullable();
|
||||
$table->decimal('svm_confidence', 5, 4)->nullable();
|
||||
$table->decimal('normalized_distance', 8, 4)->nullable();
|
||||
$table->string('verification_status', 30)->nullable();
|
||||
$table->boolean('is_match')->default(false);
|
||||
$table->decimal('blur_score', 6, 1)->nullable();
|
||||
$table->decimal('skor_kepercayaan', 5, 4)->nullable();
|
||||
$table->decimal('skor_svm', 5, 4)->nullable();
|
||||
$table->decimal('jarak_normalisasi', 8, 4)->nullable();
|
||||
$table->string('status_verifikasi', 30)->nullable();
|
||||
$table->boolean('apakah_cocok')->default(false);
|
||||
$table->decimal('skor_blur', 6, 1)->nullable();
|
||||
$table->string('tipe', 10)->default('presensi');
|
||||
$table->timestamps();
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,10 @@ public function run(): void
|
|||
$tglMulai = now()->addDays(rand(-10, 10));
|
||||
$tglSelesai = (clone $tglMulai)->addDays(rand(1, 3));
|
||||
|
||||
$idIzin = 'IZN-' . date('ym') . '-' . strtoupper(Str::random(5));
|
||||
|
||||
DB::table('pengajuan_izin')->insertOrIgnore([
|
||||
'id_izin' => $idIzin,
|
||||
'id_user' => $user->id,
|
||||
'id_jenis_izin' => $idJenis,
|
||||
'tanggal_mulai' => $tglMulai->format('Y-m-d'),
|
||||
|
|
|
|||
|
|
@ -76,13 +76,17 @@
|
|||
// ==== Polling Badge Notifikasi ====
|
||||
function updateNotifBadge() {
|
||||
const badge = document.getElementById('notif-badge');
|
||||
if (!badge) return;
|
||||
if (!badge) return; // User tidak punya akses notifikasi, hentikan tanpa fetch
|
||||
|
||||
fetch('/notifikasi/unread-count', {
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' }
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(r => {
|
||||
if (!r.ok) return null; // Tangani 403/redirect dengan aman
|
||||
return r.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (!data) return;
|
||||
const count = data.count ?? 0;
|
||||
if (count > 0) {
|
||||
badge.textContent = count > 99 ? '99+' : count;
|
||||
|
|
@ -215,7 +219,7 @@ function confirmDelete(id) {
|
|||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function confirmAction(event, formId, message, confirmBtnColor = '#3085d6', confirmBtnText = 'Ya, lanjutkan!') {
|
||||
event.preventDefault();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
opencv-contrib-python-headless
|
||||
numpy
|
||||
scikit-learn
|
||||
scikit-image
|
||||
joblib
|
||||
Pillow
|
||||
|
|
|
|||
|
|
@ -199,7 +199,7 @@ def verify_face(model_dir, user_id, image_path):
|
|||
"expected_user": expected_user,
|
||||
"unknown_confidence": round(unknown_confidence, 4),
|
||||
"blur_score": round(blur_score, 1),
|
||||
"user_id": int(user_id)
|
||||
"user_id": int(user_id) if str(user_id).isdigit() else user_id
|
||||
}
|
||||
|
||||
print(json.dumps(result))
|
||||
|
|
|
|||
|
|
@ -1,28 +1,29 @@
|
|||
@props(['label' => null, 'name', 'value' => ''])
|
||||
@props(['label' => null, 'name', 'value' => ''])
|
||||
|
||||
@once
|
||||
<style>
|
||||
input[type="date"]::-webkit-calendar-picker-indicator,
|
||||
input[type="date"]::-webkit-inner-spin-button {
|
||||
display: none;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
</style>
|
||||
@endonce
|
||||
|
||||
<div class="space-y-1">
|
||||
@if($label)
|
||||
<label class="text-sm font-semibold text-slate-700">{{ $label }}</label>
|
||||
@endif
|
||||
|
||||
<div class="relative">
|
||||
<style>
|
||||
/* Sembunyikan icon default browser */
|
||||
input[type="date"]::-webkit-calendar-picker-indicator,
|
||||
input[type="date"]::-webkit-inner-spin-button {
|
||||
display: none;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<input type="date"
|
||||
name="{{ $name }}"
|
||||
<div class="relative">
|
||||
<input type="date"
|
||||
name="{{ $name }}"
|
||||
value="{{ old($name, $value) }}"
|
||||
onclick="try{this.showPicker()}catch(e){}"
|
||||
onfocus="try{this.showPicker()}catch(e){}"
|
||||
{{ $attributes->merge(['class' => 'w-full pl-4 pr-10 py-2.5 bg-white border border-slate-300 rounded-xl text-sm outline-none focus:ring-2 focus:ring-[#130F26] focus:border-[#130F26] transition-all text-slate-600 cursor-pointer appearance-none']) }}
|
||||
>
|
||||
|
||||
|
||||
<div class="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none text-slate-500">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
||||
|
|
|
|||
|
|
@ -138,10 +138,11 @@ function toggleResetBtn(value) {
|
|||
function openEditModal(id, nama, sisa) {
|
||||
document.getElementById('editNama').value = nama;
|
||||
document.getElementById('editSisa').value = sisa;
|
||||
|
||||
|
||||
let form = document.getElementById('formEditCuti');
|
||||
form.action = `/cuti/${id}`;
|
||||
|
||||
let baseUrl = "{{ route('cuti.update', ':id') }}";
|
||||
form.action = baseUrl.replace(':id', id);
|
||||
|
||||
window.dispatchEvent(new CustomEvent('open-modal', { detail: 'edit-cuti' }));
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -224,7 +224,7 @@ class="text-xs text-primary hover:underline font-medium">Lihat</a>
|
|||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
@section('script')
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script>
|
||||
const ctx = document.getElementById('attendanceChart').getContext('2d');
|
||||
|
|
@ -261,4 +261,4 @@ class="text-xs text-primary hover:underline font-medium">Lihat</a>
|
|||
}
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
@endsection
|
||||
|
|
|
|||
|
|
@ -1,7 +1,25 @@
|
|||
@extends('layouts.app')
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Data Divisi')
|
||||
|
||||
@section('script')
|
||||
<script>
|
||||
window.openEditModal = function (btn) {
|
||||
const id = btn.dataset.id;
|
||||
const nama = btn.dataset.nama;
|
||||
|
||||
document.getElementById('edit-id').value = id;
|
||||
document.getElementById('edit-nama').value = nama;
|
||||
|
||||
let form = document.getElementById('form-edit');
|
||||
let baseUrl = "{{ route('divisi.update', ':id') }}";
|
||||
form.action = baseUrl.replace(':id', id);
|
||||
|
||||
window.dispatchEvent(new CustomEvent('open-modal', { detail: 'edit-divisi' }));
|
||||
}
|
||||
</script>
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
<div class="space-y-6">
|
||||
|
||||
|
|
@ -84,12 +102,7 @@ class="px-4 py-2 text-sm font-medium text-white bg-primary rounded-xl hover:bg-p
|
|||
|
||||
<input type="hidden" id="edit-id" name="id">
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700 mb-1">Nama Divisi</label>
|
||||
|
||||
<input type="text" id="edit-nama" name="nama_divisi" required
|
||||
class="w-full px-4 py-2 border border-slate-300 rounded-xl focus:ring-2 focus:ring-[#130F26] outline-none transition">
|
||||
</div>
|
||||
<x-input label="Nama Divisi" name="nama_divisi" id="edit-nama" required />
|
||||
|
||||
<div class="flex justify-end gap-3 pt-4">
|
||||
<button type="button" x-data @click="$dispatch('close-modal', 'edit-divisi')"
|
||||
|
|
@ -100,20 +113,5 @@ class="px-4 py-2 text-sm font-medium text-white bg-amber-500 rounded-xl hover:bg
|
|||
</form>
|
||||
</x-modal>
|
||||
|
||||
<script>
|
||||
window.openEditModal = function (btn) {
|
||||
const id = btn.dataset.id;
|
||||
const nama = btn.dataset.nama;
|
||||
|
||||
document.getElementById('edit-id').value = id;
|
||||
document.getElementById('edit-nama').value = nama;
|
||||
|
||||
let form = document.getElementById('form-edit');
|
||||
let baseUrl = "{{ route('divisi.update', ':id') }}";
|
||||
form.action = baseUrl.replace(':id', id);
|
||||
|
||||
window.dispatchEvent(new CustomEvent('open-modal', { detail: 'edit-divisi' }));
|
||||
}
|
||||
</script>
|
||||
|
||||
@endsection
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
@php use App\Enums\StatusVerifikasiWajah; @endphp
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Verifikasi Wajah')
|
||||
|
|
@ -109,12 +110,13 @@ class="!mb-0" />
|
|||
|
||||
@forelse ($users as $user)
|
||||
@php
|
||||
|
||||
$dw = $user->dataWajah;
|
||||
$faceStatus = 'unregistered';
|
||||
if ($dw) {
|
||||
if ($dw->is_verified == 0) $faceStatus = 'pending';
|
||||
elseif ($dw->is_verified == 1) $faceStatus = 'approved';
|
||||
elseif ($dw->is_verified == 2) $faceStatus = 'rejected';
|
||||
if ($dw->is_verified == StatusVerifikasiWajah::PENDING) $faceStatus = 'pending';
|
||||
elseif ($dw->is_verified == StatusVerifikasiWajah::APPROVED) $faceStatus = 'approved';
|
||||
elseif ($dw->is_verified == StatusVerifikasiWajah::REJECTED) $faceStatus = 'rejected';
|
||||
}
|
||||
@endphp
|
||||
<tr class="hover:bg-slate-50 transition-colors">
|
||||
|
|
|
|||
|
|
@ -1,15 +1,154 @@
|
|||
@extends('layouts.app')
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Data Hari Libur')
|
||||
|
||||
@section('style')
|
||||
<script src='https://cdn.jsdelivr.net/npm/fullcalendar@6.1.8/index.global.min.js'></script>
|
||||
<style>
|
||||
.fc-toolbar-title { font-size: 1.15rem !important; font-weight: 700; color: #1e293b; }
|
||||
.fc-col-header-cell { background-color: #f8fafc; padding: 8px 0; border-color: #e2e8f0; }
|
||||
.fc-daygrid-day-number { color: #64748b; font-weight: 500; font-size: 0.85rem; }
|
||||
.fc-day-today { background-color: #f0f9ff !important; }
|
||||
.fc-event { border: none; padding: 2px; }
|
||||
.fc-event {
|
||||
cursor: default;
|
||||
border: none !important;
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
background: transparent;
|
||||
}
|
||||
.fc-daygrid-event {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.fc-toolbar-title {
|
||||
font-size: 1.25rem !important;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
}
|
||||
.fc-col-header-cell {
|
||||
background-color: #f8fafc;
|
||||
padding: 12px 0;
|
||||
border-color: #e2e8f0;
|
||||
}
|
||||
.fc-daygrid-day-number {
|
||||
color: #64748b;
|
||||
font-weight: 500;
|
||||
}
|
||||
.fc-day-today {
|
||||
background-color: #ffffff !important;
|
||||
}
|
||||
.fc-event-title {
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.fc-popover-body {
|
||||
max-height: 250px !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
#holiday-calendar {
|
||||
min-height: 550px;
|
||||
font-family: inherit;
|
||||
}
|
||||
.fc-theme-standard td, .fc-theme-standard th, .fc-theme-standard .fc-scrollgrid {
|
||||
border-color: #f1f5f9;
|
||||
}
|
||||
.fc-theme-standard .fc-scrollgrid { border-radius: 12px; overflow: hidden; border: 1px solid #e2e8f0; }
|
||||
.fc-col-header-cell {
|
||||
background-color: #f8fafc;
|
||||
padding: 12px 0 !important;
|
||||
}
|
||||
.fc-col-header-cell-cushion {
|
||||
color: #64748b;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
.fc-daygrid-day-top {
|
||||
justify-content: center !important;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
.fc-daygrid-day-number {
|
||||
color: #475569;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
text-decoration: none !important;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 9999px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.fc-daygrid-day-number:hover {
|
||||
background-color: #f1f5f9;
|
||||
}
|
||||
.fc-day-today .fc-daygrid-day-number {
|
||||
color: #ffffff !important;
|
||||
background-color: #3b82f6 !important;
|
||||
box-shadow: 0 2px 4px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
.fc-daygrid-day-events {
|
||||
padding: 0 6px !important;
|
||||
}
|
||||
.fc-daygrid-event-harness {
|
||||
margin-bottom: 6px !important;
|
||||
}
|
||||
.fc-event {
|
||||
border: none !important;
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
border-radius: 6px !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
.fc-daygrid-more-link {
|
||||
color: #ef4444 !important;
|
||||
font-weight: 700;
|
||||
font-size: 0.7rem;
|
||||
padding: 5px 8px;
|
||||
background-color: #fef2f2;
|
||||
border-radius: 6px;
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin: 4px 6px 8px 6px;
|
||||
transition: all 0.2s;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
.fc-daygrid-more-link:hover {
|
||||
background-color: #fee2e2;
|
||||
color: #dc2626 !important;
|
||||
}
|
||||
.fc .fc-toolbar-title {
|
||||
font-size: 1.25rem !important;
|
||||
font-weight: 800;
|
||||
color: #0f172a;
|
||||
}
|
||||
.fc .fc-button-primary {
|
||||
background-color: #ffffff !important;
|
||||
color: #475569 !important;
|
||||
border: 1px solid #e2e8f0 !important;
|
||||
border-radius: 8px !important;
|
||||
font-weight: 600;
|
||||
text-transform: capitalize;
|
||||
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||
padding: 6px 16px !important;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.fc .fc-button-primary:hover {
|
||||
background-color: #f8fafc !important;
|
||||
color: #0f172a !important;
|
||||
border-color: #cbd5e1 !important;
|
||||
}
|
||||
.fc .fc-button-primary:not(:disabled).fc-button-active,
|
||||
.fc .fc-button-primary:not(:disabled):active {
|
||||
background-color: #f1f5f9 !important;
|
||||
color: #0f172a !important;
|
||||
border-color: #cbd5e1 !important;
|
||||
box-shadow: inset 0 2px 4px 0 rgb(0 0 0 / 0.05) !important;
|
||||
}
|
||||
.fc-toolbar.fc-header-toolbar {
|
||||
margin-bottom: 24px !important;
|
||||
}
|
||||
</style>
|
||||
@endsection
|
||||
|
||||
|
|
@ -22,7 +161,7 @@
|
|||
<x-filter-select name="id_kantor" onchange="this.form.submit()" class="py-1.5 text-xs w-48">
|
||||
<option value="">Semua Kantor (Global)</option>
|
||||
@foreach($kantors as $k)
|
||||
<option value="{{ $k->id_kantor }}" request('id_kantor') == $k->id_kantor ? 'selected' : ''>{{ $k->nama_kantor }}</option>
|
||||
<option value="{{ $k->id_kantor }}" {{ request('id_kantor') == $k->id_kantor ? 'selected' : '' }}>{{ $k->nama_kantor }}</option>
|
||||
@endforeach
|
||||
</x-filter-select>
|
||||
</form>
|
||||
|
|
@ -48,7 +187,7 @@
|
|||
<svg class="w-5 h-5 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg>
|
||||
Kalender Libur Nasional & Perusahaan
|
||||
</h3>
|
||||
<div id="calendar" class="min-h-[450px]"></div>
|
||||
<div id="holiday-calendar" class="min-h-[450px]"></div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
|
||||
|
|
@ -154,6 +293,9 @@
|
|||
</form>
|
||||
</x-modal>
|
||||
|
||||
@endsection
|
||||
|
||||
@section('script')
|
||||
<script>
|
||||
window.openEditModal = function (btn) {
|
||||
const id = btn.dataset.id;
|
||||
|
|
@ -176,7 +318,7 @@
|
|||
window.holidayCalendar = window.holidayCalendar || null;
|
||||
|
||||
function initHolidayCalendar() {
|
||||
var calendarEl = document.getElementById('calendar');
|
||||
var calendarEl = document.getElementById('holiday-calendar');
|
||||
if (!calendarEl) return;
|
||||
|
||||
if (window.holidayCalendar) {
|
||||
|
|
@ -184,8 +326,14 @@ function initHolidayCalendar() {
|
|||
window.holidayCalendar = null;
|
||||
}
|
||||
|
||||
// Paksa destroy jadwal calendar jika masih hidup dari halaman penjadwalan
|
||||
if (window.jadwalCalendar) {
|
||||
window.jadwalCalendar.destroy();
|
||||
window.jadwalCalendar = null;
|
||||
}
|
||||
|
||||
const rawData = @json($hariLiburs->items() ?? $hariLiburs);
|
||||
|
||||
|
||||
const calendarEvents = rawData.map(function(hl) {
|
||||
return {
|
||||
title: hl.keterangan,
|
||||
|
|
@ -204,14 +352,25 @@ function initHolidayCalendar() {
|
|||
center: 'title',
|
||||
right: 'dayGridMonth'
|
||||
},
|
||||
buttonText: {
|
||||
today: 'Hari Ini',
|
||||
month: 'Bulan',
|
||||
},
|
||||
events: calendarEvents,
|
||||
height: 'auto',
|
||||
displayEventTime: false,
|
||||
dayMaxEvents: 3,
|
||||
moreLinkText: 'lainnya',
|
||||
moreLinkClick: 'popover',
|
||||
eventContent: function(arg) {
|
||||
return {
|
||||
html: `<div style="background:#fef2f2; color:#dc2626; padding: 2px 4px; border-radius: 4px; border-left: 3px solid #ef4444; font-weight:700; font-size: 10px; white-space: normal; line-height: 1.2;">
|
||||
${arg.event.title}
|
||||
</div>`
|
||||
};
|
||||
let contentEl = document.createElement('div');
|
||||
contentEl.innerHTML = `
|
||||
<div class="bg-red-50 text-red-700 px-2 py-1.5 rounded-lg border border-red-200 font-bold text-[10px] flex items-center gap-1.5 w-full truncate shadow-sm">
|
||||
<span class="w-2 h-2 rounded-full bg-red-500 flex-shrink-0"></span>
|
||||
<span class="truncate">${arg.event.title}</span>
|
||||
</div>
|
||||
`;
|
||||
return { domNodes: [contentEl] };
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -222,7 +381,7 @@ function initHolidayCalendar() {
|
|||
window.__holidayEventAttached = true;
|
||||
|
||||
document.addEventListener('turbo:load', function () {
|
||||
if (document.getElementById('calendar') && typeof initHolidayCalendar === 'function') {
|
||||
if (document.getElementById('holiday-calendar') && typeof initHolidayCalendar === 'function') {
|
||||
initHolidayCalendar();
|
||||
}
|
||||
});
|
||||
|
|
@ -236,12 +395,12 @@ function initHolidayCalendar() {
|
|||
}
|
||||
|
||||
if (document.readyState === 'complete' || document.readyState === 'interactive') {
|
||||
if (document.getElementById('calendar') && !window.holidayCalendar) {
|
||||
if (document.getElementById('holiday-calendar') && !window.holidayCalendar) {
|
||||
setTimeout(initHolidayCalendar, 50);
|
||||
}
|
||||
} else {
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
if (document.getElementById('calendar') && !window.holidayCalendar) {
|
||||
if (document.getElementById('holiday-calendar') && !window.holidayCalendar) {
|
||||
setTimeout(initHolidayCalendar, 50);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -31,9 +31,9 @@ class="!mb-0 w-full md:w-64" />
|
|||
<x-filter-select name="status"
|
||||
onchange="window.location.href='{{ route('izin.index') }}?status=' + this.value + '&search={{ request('search') }}'">
|
||||
<option value="">Semua Status</option>
|
||||
<option value="1" {{ $statusId == 1 ? 'selected' : '' }}>Pending</option>
|
||||
<option value="2" {{ $statusId == 2 ? 'selected' : '' }}>Disetujui</option>
|
||||
<option value="3" {{ $statusId == 3 ? 'selected' : '' }}>Ditolak</option>
|
||||
<option value="{{ \App\Enums\StatusPengajuan::PENDING }}" {{ $statusId == \App\Enums\StatusPengajuan::PENDING ? 'selected' : '' }}>Pending</option>
|
||||
<option value="{{ \App\Enums\StatusPengajuan::DISETUJUI }}" {{ $statusId == \App\Enums\StatusPengajuan::DISETUJUI ? 'selected' : '' }}>Disetujui</option>
|
||||
<option value="{{ \App\Enums\StatusPengajuan::DITOLAK }}" {{ $statusId == \App\Enums\StatusPengajuan::DITOLAK ? 'selected' : '' }}>Ditolak</option>
|
||||
</x-filter-select>
|
||||
</div>
|
||||
</x-page-header>
|
||||
|
|
@ -66,7 +66,7 @@ class="w-10 h-10 rounded-full bg-slate-100 flex items-center justify-center text
|
|||
|
||||
<span
|
||||
class="px-2.5 py-1 rounded-full text-xs font-medium
|
||||
{{ $item->id_jenis_izin == 1 ? 'bg-rose-50 text-rose-600' : 'bg-blue-50 text-blue-600' }}">
|
||||
{{ $item->id_jenis_izin == \App\Enums\JenisIzin::SAKIT ? 'bg-rose-50 text-rose-600' : 'bg-blue-50 text-blue-600' }}">
|
||||
{{ $item->jenisIzin->nama_izin ?? 'Unknown' }}
|
||||
</span>
|
||||
</td>
|
||||
|
|
@ -81,12 +81,12 @@ class="px-2.5 py-1 rounded-full text-xs font-medium
|
|||
@php
|
||||
$statusName = $item->statusPengajuan->nama_status ?? '-';
|
||||
$badgeColor = match ($item->id_status) {
|
||||
2 => 'green',
|
||||
3 => 'red',
|
||||
\App\Enums\StatusPengajuan::DISETUJUI => 'green',
|
||||
\App\Enums\StatusPengajuan::DITOLAK => 'red',
|
||||
default => 'yellow'
|
||||
};
|
||||
@endphp
|
||||
@if($item->id_jenis_izin == 2 && $item->id_status == 1)
|
||||
@if($item->id_jenis_izin == \App\Enums\JenisIzin::CUTI && $item->id_status == \App\Enums\StatusPengajuan::PENDING)
|
||||
<x-badge color="blue">Via Surat Izin</x-badge>
|
||||
@else
|
||||
<x-badge color="{{ $badgeColor }}">{{ $statusName }}</x-badge>
|
||||
|
|
@ -94,12 +94,23 @@ class="px-2.5 py-1 rounded-full text-xs font-medium
|
|||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<button data-item="{{ $item->makeHidden('suratIzin')->toJson() }}"
|
||||
<button data-item="{{ json_encode([
|
||||
'id_izin' => $item->id_izin,
|
||||
'id_status' => $item->id_status,
|
||||
'id_jenis_izin' => $item->id_jenis_izin,
|
||||
'tanggal_mulai' => $item->tanggal_mulai,
|
||||
'tanggal_selesai' => $item->tanggal_selesai,
|
||||
'alasan' => $item->alasan,
|
||||
'alasan_penolakan' => $item->alasan_penolakan,
|
||||
'bukti_file' => $item->bukti_file,
|
||||
'user' => ['nama_lengkap' => $item->user?->nama_lengkap, 'nik' => $item->user?->nik],
|
||||
'jenis_izin' => ['nama_izin' => $item->jenisIzin?->nama_izin],
|
||||
]) }}"
|
||||
onclick="openDetailModal(JSON.parse(this.getAttribute('data-item')))"
|
||||
class="px-3 py-1.5 text-xs font-medium text-slate-600 bg-white border border-slate-300 rounded-lg hover:bg-slate-50 transition shadow-sm">
|
||||
Detail
|
||||
</button>
|
||||
@if($item->id_jenis_izin == 2 && $item->suratIzin)
|
||||
@if($item->id_jenis_izin == \App\Enums\JenisIzin::CUTI && $item->suratIzin)
|
||||
<a href="{{ route('surat-izin.show', $item->suratIzin->id_surat) }}"
|
||||
class="px-3 py-1.5 text-xs font-medium text-blue-600 bg-blue-50 border border-blue-200 rounded-lg hover:bg-blue-100 transition shadow-sm">
|
||||
Lihat Surat →
|
||||
|
|
@ -150,7 +161,7 @@ class="px-3 py-1.5 text-xs font-medium text-blue-600 bg-blue-50 border border-bl
|
|||
|
||||
<div>
|
||||
<x-select label="Jenis Izin" name="id_jenis_izin" required>
|
||||
@foreach($jenisIzin as $j)
|
||||
@foreach($jenisIzinList as $j)
|
||||
<option value="{{ $j->id_jenis_izin }}">{{ $j->nama_izin }}</option>
|
||||
@endforeach
|
||||
</x-select>
|
||||
|
|
@ -165,7 +176,7 @@ class="px-3 py-1.5 text-xs font-medium text-blue-600 bg-blue-50 border border-bl
|
|||
<x-textarea label="Alasan" name="alasan" required rows="3" />
|
||||
</div>
|
||||
|
||||
<x-input type="file" label="Bukti Dokumen (Gambar/PDF)" name="bukti_file" required
|
||||
<x-input type="file" label="Bukti Dokumen (Gambar/PDF)" name="bukti_file"
|
||||
accept=".jpeg,.png,.jpg,.pdf" />
|
||||
|
||||
<div class="pt-4 flex justify-end gap-3">
|
||||
|
|
@ -203,6 +214,11 @@ class="px-3 py-1.5 text-xs font-medium text-blue-600 bg-blue-50 border border-bl
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<div id="alasan-penolakan-wrapper" class="hidden">
|
||||
<label class="block text-xs font-bold text-red-500 uppercase tracking-wider mb-1">Alasan Penolakan</label>
|
||||
<p class="text-sm text-red-700 bg-red-50 p-3 rounded-lg border border-red-100" id="detail-alasan-penolakan">-</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-500 uppercase tracking-wider mb-1">Bukti Dokumen</label>
|
||||
<div
|
||||
|
|
@ -234,12 +250,13 @@ class="text-sm text-blue-600 hover:underline hidden">Download File</a>
|
|||
@click="$dispatch('close-modal', 'detail-izin')">Tutup</x-button>
|
||||
|
||||
@auth
|
||||
|
||||
<form id="form-reject-izin" action="" method="POST" class="hidden">@csrf</form>
|
||||
<form id="form-reject-izin" action="" method="POST" class="hidden">
|
||||
@csrf
|
||||
<input type="hidden" name="alasan_penolakan" id="input-alasan-penolakan">
|
||||
</form>
|
||||
<form id="form-approve-izin" action="" method="POST" class="hidden">@csrf</form>
|
||||
|
||||
<x-button id="btn-reject" variant="danger"
|
||||
onclick="confirmAction(event, 'form-reject-izin', 'Status akan diubah menjadi Ditolak.', '#ef4444', 'Tolak')">
|
||||
<x-button id="btn-reject" variant="danger" onclick="submitRejectWithAlasan()">
|
||||
Tolak
|
||||
</x-button>
|
||||
<x-button id="btn-approve" variant="primary"
|
||||
|
|
@ -258,6 +275,10 @@ class="text-sm text-blue-600 hover:underline hidden">Download File</a>
|
|||
|
||||
@section('script')
|
||||
<script>
|
||||
const JENIS_CUTI = {{ \App\Enums\JenisIzin::CUTI }};
|
||||
const STATUS_PENDING = {{ \App\Enums\StatusPengajuan::PENDING }};
|
||||
const STATUS_DITOLAK = {{ \App\Enums\StatusPengajuan::DITOLAK }};
|
||||
|
||||
function openDetailModal(data) {
|
||||
document.getElementById('current-id-izin').value = data.id_izin;
|
||||
document.getElementById('detail-nama').innerText = data.user.nama_lengkap;
|
||||
|
|
@ -281,20 +302,16 @@ function openDetailModal(data) {
|
|||
noBukti.classList.add('hidden');
|
||||
|
||||
if (data.bukti_file) {
|
||||
const url = "{{ asset('storage') }}/" + data.bukti_file;
|
||||
const fileUrl = "{{ asset('storage') }}/" + data.bukti_file;
|
||||
const isImage = data.bukti_file.match(/\.(jpeg|jpg|png|gif)$/i) || data.bukti_file.includes('placehold.co');
|
||||
|
||||
if (isImage) {
|
||||
img.src = url;
|
||||
img.src = data.bukti_file.startsWith('http') ? data.bukti_file : fileUrl;
|
||||
img.classList.remove('hidden');
|
||||
} else {
|
||||
link.href = url;
|
||||
link.href = fileUrl;
|
||||
link.classList.remove('hidden');
|
||||
}
|
||||
|
||||
if (data.bukti_file.startsWith('http')) {
|
||||
img.src = data.bukti_file;
|
||||
}
|
||||
} else {
|
||||
noBukti.classList.remove('hidden');
|
||||
}
|
||||
|
|
@ -302,13 +319,20 @@ function openDetailModal(data) {
|
|||
const btnApprove = document.getElementById('btn-approve');
|
||||
const btnReject = document.getElementById('btn-reject');
|
||||
const cutiInfo = document.getElementById('cuti-info-banner');
|
||||
const alasanPenolakanWrapper = document.getElementById('alasan-penolakan-wrapper');
|
||||
|
||||
if (btnApprove) btnApprove.classList.add('hidden');
|
||||
if (btnReject) btnReject.classList.add('hidden');
|
||||
if (cutiInfo) cutiInfo.classList.add('hidden');
|
||||
if (alasanPenolakanWrapper) alasanPenolakanWrapper.classList.add('hidden');
|
||||
|
||||
if (data.id_status == 1) {
|
||||
if (data.id_jenis_izin == 2) {
|
||||
if (data.id_status == STATUS_DITOLAK && data.alasan_penolakan) {
|
||||
document.getElementById('detail-alasan-penolakan').innerText = data.alasan_penolakan;
|
||||
if (alasanPenolakanWrapper) alasanPenolakanWrapper.classList.remove('hidden');
|
||||
}
|
||||
|
||||
if (data.id_status == STATUS_PENDING) {
|
||||
if (data.id_jenis_izin == JENIS_CUTI) {
|
||||
if (cutiInfo) cutiInfo.classList.remove('hidden');
|
||||
} else {
|
||||
if (btnApprove) btnApprove.classList.remove('hidden');
|
||||
|
|
@ -319,6 +343,15 @@ function openDetailModal(data) {
|
|||
window.dispatchEvent(new CustomEvent('open-modal', { detail: 'detail-izin' }));
|
||||
}
|
||||
|
||||
function submitRejectWithAlasan() {
|
||||
const alasan = prompt('Masukkan alasan penolakan (opsional):');
|
||||
if (alasan === null) return;
|
||||
const inputAlasan = document.getElementById('input-alasan-penolakan');
|
||||
if (inputAlasan) inputAlasan.value = alasan;
|
||||
const form = document.getElementById('form-reject-izin');
|
||||
if (form) form.submit();
|
||||
}
|
||||
|
||||
function initCreateIzinForm() {
|
||||
const createForm = document.querySelector('form[action="{{ route('izin.store') }}"]');
|
||||
if (createForm) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,25 @@
|
|||
@extends('layouts.app')
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Data Jabatan')
|
||||
|
||||
@section('script')
|
||||
<script>
|
||||
window.openEditModal = function (btn) {
|
||||
const id = btn.dataset.id;
|
||||
const nama = btn.dataset.nama;
|
||||
|
||||
document.getElementById('edit-id').value = id;
|
||||
document.getElementById('edit-nama').value = nama;
|
||||
|
||||
let form = document.getElementById('form-edit');
|
||||
let baseUrl = "{{ route('jabatan.update', ':id') }}";
|
||||
form.action = baseUrl.replace(':id', id);
|
||||
|
||||
window.dispatchEvent(new CustomEvent('open-modal', { detail: 'edit-jabatan' }));
|
||||
}
|
||||
</script>
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
<div class="space-y-6">
|
||||
|
||||
|
|
@ -80,11 +98,7 @@ class="px-4 py-2 text-sm font-medium text-white bg-primary rounded-xl hover:bg-p
|
|||
|
||||
<input type="hidden" id="edit-id" name="id">
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700 mb-1">Nama Jabatan</label>
|
||||
<input type="text" id="edit-nama" name="nama_jabatan" required
|
||||
class="w-full px-4 py-2 border border-slate-300 rounded-xl focus:ring-2 focus:ring-[#130F26] outline-none transition">
|
||||
</div>
|
||||
<x-input label="Nama Jabatan" name="nama_jabatan" id="edit-nama" required />
|
||||
|
||||
<div class="flex justify-end gap-3 pt-4">
|
||||
<button type="button" x-data @click="$dispatch('close-modal', 'edit-jabatan')"
|
||||
|
|
@ -95,20 +109,6 @@ class="px-4 py-2 text-sm font-medium text-white bg-amber-500 rounded-xl hover:bg
|
|||
</form>
|
||||
</x-modal>
|
||||
|
||||
<script>
|
||||
window.openEditModal = function (btn) {
|
||||
const id = btn.dataset.id;
|
||||
const nama = btn.dataset.nama;
|
||||
|
||||
document.getElementById('edit-id').value = id;
|
||||
document.getElementById('edit-nama').value = nama;
|
||||
|
||||
let form = document.getElementById('form-edit');
|
||||
let baseUrl = "{{ route('jabatan.update', ':id') }}";
|
||||
form.action = baseUrl.replace(':id', id);
|
||||
|
||||
window.dispatchEvent(new CustomEvent('open-modal', { detail: 'edit-jabatan' }));
|
||||
}
|
||||
</script>
|
||||
|
||||
@endsection
|
||||
|
|
|
|||
|
|
@ -13,75 +13,23 @@
|
|||
<form action="{{ route('jadwal.store') }}" method="POST">
|
||||
@csrf
|
||||
|
||||
<style>
|
||||
input[type="date"]::-webkit-calendar-picker-indicator,
|
||||
input[type="date"]::-webkit-inner-spin-button {
|
||||
display: none;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
</style>
|
||||
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.5rem;" class="mb-6">
|
||||
<div class="space-y-1">
|
||||
<label class="text-sm font-semibold text-slate-700">Tanggal Mulai</label>
|
||||
<div class="relative">
|
||||
<input type="date" name="tanggal_mulai" value="{{ old('tanggal_mulai', now()->format('Y-m-d')) }}" required
|
||||
onclick="try{this.showPicker()}catch(e){}"
|
||||
onfocus="try{this.showPicker()}catch(e){}"
|
||||
class="w-full pl-4 pr-10 py-2.5 bg-white border border-slate-300 rounded-xl text-sm outline-none focus:ring-2 focus:ring-[#130F26] focus:border-[#130F26] transition-all text-slate-600 cursor-pointer appearance-none"
|
||||
style="-webkit-appearance: none;">
|
||||
<div class="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none text-slate-500">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
@error('tanggal_mulai')
|
||||
<p class="text-red-500 text-xs mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
|
||||
<x-date-input label="Tanggal Mulai" name="tanggal_mulai"
|
||||
value="{{ old('tanggal_mulai', now()->format('Y-m-d')) }}" required />
|
||||
|
||||
<div class="space-y-1">
|
||||
<label class="text-sm font-semibold text-slate-700">Tanggal Selesai</label>
|
||||
<div class="relative">
|
||||
<input type="date" name="tanggal_selesai" value="{{ old('tanggal_selesai', now()->addDays(6)->format('Y-m-d')) }}" required
|
||||
onclick="try{this.showPicker()}catch(e){}"
|
||||
onfocus="try{this.showPicker()}catch(e){}"
|
||||
class="w-full pl-4 pr-10 py-2.5 bg-white border border-slate-300 rounded-xl text-sm outline-none focus:ring-2 focus:ring-[#130F26] focus:border-[#130F26] transition-all text-slate-600 cursor-pointer appearance-none"
|
||||
style="-webkit-appearance: none;">
|
||||
<div class="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none text-slate-500">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
@error('tanggal_selesai')
|
||||
<p class="text-red-500 text-xs mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
<x-date-input label="Tanggal Selesai" name="tanggal_selesai"
|
||||
value="{{ old('tanggal_selesai', now()->addDays(6)->format('Y-m-d')) }}" required />
|
||||
|
||||
<div class="space-y-1">
|
||||
<label class="text-sm font-semibold text-slate-700">Shift</label>
|
||||
<div class="relative">
|
||||
<select name="id_shift" id="id_shift" required
|
||||
class="w-full pl-4 pr-10 py-2.5 bg-white border border-slate-300 rounded-xl text-sm outline-none focus:ring-2 focus:ring-[#130F26] focus:border-[#130F26] transition-all text-slate-600 cursor-pointer appearance-none">
|
||||
<option value="">-- Pilih Shift --</option>
|
||||
@foreach($shifts as $shift)
|
||||
<option value="{{ $shift->id_shift }}">{{ $shift->nama_shift }}
|
||||
({{ \Carbon\Carbon::parse($shift->jam_mulai)->format('H:i') }} -
|
||||
{{ \Carbon\Carbon::parse($shift->jam_selesai)->format('H:i') }})
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<div class="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none text-slate-500">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
@error('id_shift')
|
||||
<p class="text-red-500 text-xs mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
<x-select label="Shift" name="id_shift" id="id_shift" class="!mb-0" required>
|
||||
<option value="">-- Pilih Shift --</option>
|
||||
@foreach($shifts as $shift)
|
||||
<option value="{{ $shift->id_shift }}" {{ old('id_shift') == $shift->id_shift ? 'selected' : '' }}>
|
||||
{{ $shift->nama_shift }}
|
||||
({{ \Carbon\Carbon::parse($shift->jam_mulai)->format('H:i') }} -
|
||||
{{ \Carbon\Carbon::parse($shift->jam_selesai)->format('H:i') }})
|
||||
</option>
|
||||
@endforeach
|
||||
</x-select>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
|
|
|
|||
|
|
@ -369,7 +369,7 @@ class="text-red-500 text-sm hover:underline font-medium">Hapus Jadwal</button>
|
|||
</x-modal>
|
||||
|
||||
<x-modal name="bulk-delete" title="Hapus Jadwal Massal">
|
||||
<form action="{{ route('jadwal.bulk-delete') }}" method="POST"
|
||||
<form action="{{ route('jadwal.bulk-delete') }}" method="POST" id="form-bulk-delete"
|
||||
x-data="{
|
||||
tglMulai: '',
|
||||
tglSelesai: '',
|
||||
|
|
@ -441,7 +441,7 @@ class="user-checkbox peer sr-only">
|
|||
|
||||
<div class="flex justify-end gap-3 pt-6 border-t border-slate-100 mt-6">
|
||||
<x-button type="button" x-data @click="$dispatch('close-modal', 'bulk-delete')" variant="secondary">Batal</x-button>
|
||||
<x-button type="submit" variant="danger">Ya, Hapus Terpilih</x-button>
|
||||
<x-button type="button" variant="danger" onclick="confirmBulkDelete()">Ya, Hapus Terpilih</x-button>
|
||||
</div>
|
||||
</form>
|
||||
</x-modal>
|
||||
|
|
@ -469,7 +469,7 @@ function initCalendar() {
|
|||
window.jadwalCalendar = new FullCalendar.Calendar(calendarEl, {
|
||||
initialView: 'dayGridMonth',
|
||||
locale: 'id',
|
||||
initialDate: '2026-01-01',
|
||||
initialDate: new Date().toISOString().split('T')[0],
|
||||
headerToolbar: {
|
||||
left: 'prev,next today',
|
||||
center: 'title',
|
||||
|
|
@ -983,40 +983,44 @@ function deleteJadwal() {
|
|||
}
|
||||
}
|
||||
});
|
||||
selectAllCheckbox.addEventListener('change', function () {
|
||||
const visibleCheckboxes = document.querySelectorAll('.user-checkbox');
|
||||
visibleCheckboxes.forEach(cb => cb.checked = this.checked);
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const forms = document.querySelectorAll('form');
|
||||
|
||||
forms.forEach(form => {
|
||||
const inputMulai = form.querySelector('input[name="tanggal_mulai"]');
|
||||
const inputSelesai = form.querySelector('input[name="tanggal_selesai"]');
|
||||
|
||||
if (inputMulai && inputSelesai) {
|
||||
inputMulai.addEventListener('change', function() {
|
||||
if (this.value) {
|
||||
inputSelesai.min = this.value;
|
||||
if (inputSelesai.value && inputSelesai.value < this.value) {
|
||||
inputSelesai.value = this.value;
|
||||
}
|
||||
}
|
||||
});
|
||||
inputSelesai.addEventListener('change', function() {
|
||||
if (this.value) {
|
||||
inputMulai.max = this.value;
|
||||
if (inputMulai.value && inputMulai.value > this.value) {
|
||||
inputMulai.value = this.value;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (inputMulai.value) inputSelesai.min = inputMulai.value;
|
||||
if (inputSelesai.value) inputMulai.max = inputSelesai.value;
|
||||
function confirmBulkDelete() {
|
||||
const form = document.getElementById('form-bulk-delete');
|
||||
if (!form) return;
|
||||
|
||||
const checkedBoxes = form.querySelectorAll('.user-checkbox:checked');
|
||||
const tglMulai = form.querySelector('input[name="tanggal_mulai"]')?.value;
|
||||
const tglSelesai = form.querySelector('input[name="tanggal_selesai"]')?.value;
|
||||
|
||||
if (checkedBoxes.length === 0) {
|
||||
Swal.fire('Peringatan', 'Pilih minimal 1 pegawai untuk dihapus jadwalnya.', 'warning');
|
||||
return;
|
||||
}
|
||||
if (!tglMulai || !tglSelesai) {
|
||||
Swal.fire('Peringatan', 'Tanggal mulai dan selesai wajib diisi.', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const formatTgl = (d) => {
|
||||
const obj = new Date(d);
|
||||
return obj.toLocaleDateString('id-ID', { day: 'numeric', month: 'long', year: 'numeric' });
|
||||
};
|
||||
|
||||
Swal.fire({
|
||||
title: 'Konfirmasi Hapus Massal',
|
||||
html: `Anda akan menghapus jadwal <b>${checkedBoxes.length} pegawai</b> pada rentang:<br><b>${formatTgl(tglMulai)} — ${formatTgl(tglSelesai)}</b><br><br><span class="text-red-500 font-semibold">Tindakan ini tidak dapat dibatalkan!</span>`,
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#ef4444',
|
||||
cancelButtonColor: '#64748b',
|
||||
confirmButtonText: 'Ya, Hapus Sekarang',
|
||||
cancelButtonText: 'Batal'
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
form.submit();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
</script>
|
||||
@endsection
|
||||
|
|
|
|||
|
|
@ -1,29 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang='en'>
|
||||
|
||||
<head>
|
||||
<meta charset='utf-8' />
|
||||
<script src='https://cdn.jsdelivr.net/npm/fullcalendar@6.1.8/index.global.min.js'></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var calendarEl = document.getElementById('calendar');
|
||||
var calendar = new FullCalendar.Calendar(calendarEl, {
|
||||
initialView: 'dayGridMonth',
|
||||
initialDate: '2026-01-01',
|
||||
headerToolbar: {
|
||||
left: 'prev,next today',
|
||||
center: 'title',
|
||||
right: 'dayGridMonth,timeGridWeek,timeGridDay'
|
||||
}
|
||||
});
|
||||
calendar.render();
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Test Calendar Isolation</h1>
|
||||
<div id='calendar'></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -8,7 +8,7 @@
|
|||
<x-page-header title="Laporan Lembur" subtitle="Rekapitulasi jam dan poin lembur pegawai bulanan.">
|
||||
<x-slot:actions>
|
||||
<div class="flex gap-2">
|
||||
<a href="{{ route('laporan-lembur.exportExcel', ['bulan' => $bulan, 'tahun' => $tahun]) }}" class="inline-flex items-center justify-center px-4 py-2 border border-emerald-600 font-medium rounded-lg text-emerald-700 bg-emerald-50 hover:bg-emerald-100 transition-colors shadow-sm text-sm h-[42px]">
|
||||
<a href="{{ route('laporan-lembur.exportExcel', ['bulan' => $bulan, 'tahun' => $tahun, 'id_divisi' => $divisiId, 'search' => $search]) }}" class="inline-flex items-center justify-center px-4 py-2 border border-emerald-600 font-medium rounded-lg text-emerald-700 bg-emerald-50 hover:bg-emerald-100 transition-colors shadow-sm text-sm h-[42px]">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg>
|
||||
Export Excel
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ class="flex flex-wrap gap-4 items-end mb-6 border-b border-slate-100 pb-6">
|
|||
<x-select label="Bulan" name="bulan" class="!mb-0">
|
||||
@for ($i = 1; $i <= 12; $i++)
|
||||
<option value="{{ $i }}" {{ $bulan == $i ? 'selected' : '' }}>
|
||||
{{ DateTime::createFromFormat('!m', $i)->format('F') }}
|
||||
{{ \Carbon\Carbon::create()->month($i)->translatedFormat('F') }}
|
||||
</option>
|
||||
@endfor
|
||||
</x-select>
|
||||
|
|
@ -130,7 +130,7 @@ class="inline-flex items-center gap-2 px-4 py-2 bg-red-50 text-red-700 hover:bg-
|
|||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<x-empty-state colspan="7" message="Tidak ada data presensi pada periode ini" />
|
||||
<x-empty-state colspan="8" message="Tidak ada data presensi pada periode ini" />
|
||||
@endforelse
|
||||
</x-table>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
@section('title', 'Laporan Izin & Cuti')
|
||||
|
||||
@php use App\Enums\StatusPengajuan; @endphp
|
||||
|
||||
@section('content')
|
||||
<div class="space-y-6">
|
||||
|
||||
|
|
@ -10,6 +12,7 @@
|
|||
<div class="flex gap-2">
|
||||
<x-button type="link"
|
||||
href="{{ route('laporan.izin.export', ['bulan' => $bulan, 'tahun' => $tahun, 'id_divisi' => $divisiId, 'search' => $search]) }}"
|
||||
data-turbo="false"
|
||||
class="!bg-emerald-50 !text-emerald-700 hover:!bg-emerald-100 !border-emerald-600 !ring-0 flex items-center gap-2 h-[42px]">
|
||||
<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 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg>
|
||||
Export Excel
|
||||
|
|
@ -90,7 +93,7 @@ class="!mb-0" oninput="if(this.value.length === 0) this.form.submit()" />
|
|||
@if($izin->tanggal_mulai == $izin->tanggal_selesai)
|
||||
{{ \Carbon\Carbon::parse($izin->tanggal_mulai)->translatedFormat('d M Y') }}
|
||||
@else
|
||||
{{ \Carbon\Carbon::parse($izin->tanggal_mulai)->format('d M') }} - {{ \Carbon\Carbon::parse($izin->tanggal_selesai)->translatedFormat('d M Y') }}
|
||||
{{ \Carbon\Carbon::parse($izin->tanggal_mulai)->translatedFormat('d M') }} - {{ \Carbon\Carbon::parse($izin->tanggal_selesai)->translatedFormat('d M Y') }}
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-4 text-center whitespace-nowrap">
|
||||
|
|
@ -104,8 +107,8 @@ class="!mb-0" oninput="if(this.value.length === 0) this.form.submit()" />
|
|||
<td class="px-6 py-4 text-center whitespace-nowrap">
|
||||
@php
|
||||
$color = 'yellow'; $label = 'Menunggu';
|
||||
if($izin->id_status == 2) { $color = 'green'; $label = 'Disetujui'; }
|
||||
elseif($izin->id_status == 3) { $color = 'red'; $label = 'Ditolak'; }
|
||||
if($izin->id_status == StatusPengajuan::DISETUJUI) { $color = 'green'; $label = 'Disetujui'; }
|
||||
elseif($izin->id_status == StatusPengajuan::DITOLAK) { $color = 'red'; $label = 'Ditolak'; }
|
||||
@endphp
|
||||
<x-badge color="{{ $color }}">
|
||||
{{ $label }}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@extends('layouts.app')
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Input Lembur Manual')
|
||||
|
||||
|
|
@ -32,7 +32,7 @@
|
|||
<option value="">-- Pilih Kompensasi --</option>
|
||||
@foreach($kompensasi as $k)
|
||||
<option value="{{ $k->id_kompensasi }}" {{ old('id_kompensasi') == $k->id_kompensasi ? 'selected' : '' }}>
|
||||
{{ $k->nama_kompensasi }} ({{ $k->id_kompensasi == 2 ? 'Dapat Poin Cut' : 'Uang Lembur' }})
|
||||
{{ $k->nama_kompensasi }} ({{ $k->id_kompensasi == \App\Enums\JenisKompensasi::TAMBAHAN_POIN ? 'Dapat Poin Cuti' : 'Uang Lembur' }})
|
||||
</option>
|
||||
@endforeach
|
||||
</x-select>
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@
|
|||
{{ $item->durasi_menit }} Menit
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
@if($item->id_kompensasi == 2)
|
||||
@if($item->id_kompensasi == \App\Enums\JenisKompensasi::TAMBAHAN_POIN)
|
||||
<x-badge color="purple">⭐ Poin</x-badge>
|
||||
@else
|
||||
<x-badge color="green">💵 Uang</x-badge>
|
||||
|
|
@ -56,12 +56,11 @@
|
|||
</td>
|
||||
<td class="px-6 py-4">
|
||||
@php
|
||||
$status = strtolower($item->status->nama_status ?? '-');
|
||||
$idStatus = $item->id_status;
|
||||
$badgeColor = match ($idStatus) {
|
||||
1 => 'yellow',
|
||||
2 => 'green',
|
||||
3 => 'red',
|
||||
\App\Enums\StatusPengajuan::PENDING => 'yellow',
|
||||
\App\Enums\StatusPengajuan::DISETUJUI => 'green',
|
||||
\App\Enums\StatusPengajuan::DITOLAK => 'red',
|
||||
default => 'gray'
|
||||
};
|
||||
@endphp
|
||||
|
|
@ -70,9 +69,9 @@
|
|||
</x-badge>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
@if($item->id_status == 1)
|
||||
@if($item->id_status == \App\Enums\StatusPengajuan::PENDING)
|
||||
<div class="flex justify-end gap-2">
|
||||
|
||||
|
||||
<form action="{{ route('lembur.update', $item->id_lembur) }}" method="POST">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|||
required />
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<x-input label="Nomor Telepon" name="no_telp" placeholder="0812..." />
|
||||
<x-input label="Nomor Telepon" name="no_telp" type="tel" placeholder="0812-XXXX-XXXX" />
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<x-input label="Sisa Cuti (Hari)" name="sisa_cuti" type="number" value="12"
|
||||
|
|
@ -51,7 +51,7 @@ class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|||
<h3 class="text-lg font-bold text-slate-800 mb-4">Penempatan Kerja</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="w-full">
|
||||
<x-select label="Divisi" name="id_divisi" required>
|
||||
<x-select label="Divisi" name="id_divisi" class="!mb-0" required>
|
||||
<option value="">Pilih Divisi</option>
|
||||
@foreach ($divisi as $d)
|
||||
<option value="{{ $d->id_divisi }}">{{ $d->nama_divisi }}</option>
|
||||
|
|
@ -60,7 +60,7 @@ class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|||
</div>
|
||||
|
||||
<div class="w-full">
|
||||
<x-select label="Jabatan" name="id_jabatan" required>
|
||||
<x-select label="Jabatan" name="id_jabatan" class="!mb-0" required>
|
||||
<option value="">Pilih Jabatan</option>
|
||||
@foreach ($jabatan as $j)
|
||||
<option value="{{ $j->id_jabatan }}">{{ $j->nama_jabatan }}</option>
|
||||
|
|
@ -69,7 +69,7 @@ class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|||
</div>
|
||||
|
||||
<div class="w-full">
|
||||
<x-select label="Lokasi Kantor" name="id_kantor" required>
|
||||
<x-select label="Lokasi Kantor" name="id_kantor" class="!mb-0" required>
|
||||
<option value="">Pilih Kantor</option>
|
||||
@foreach ($kantor as $k)
|
||||
<option value="{{ $k->id_kantor }}">{{ $k->nama_kantor }}</option>
|
||||
|
|
@ -78,7 +78,7 @@ class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|||
</div>
|
||||
|
||||
<div class="w-full">
|
||||
<x-select label="Role Akses" name="id_role" required>
|
||||
<x-select label="Role Akses" name="id_role" class="!mb-0" required>
|
||||
<option value="">Pilih Role Akses</option>
|
||||
@foreach ($roles as $r)
|
||||
<option value="{{ $r->id_role }}">{{ $r->nama_role }}</option>
|
||||
|
|
@ -86,7 +86,7 @@ class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|||
</x-select>
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-2 w-full mt-2">
|
||||
<div class="md:col-span-2 w-full">
|
||||
<x-date-input label="Tanggal Bergabung" name="tgl_bergabung" required />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|||
<x-input label="Email" name="email" type="email" value="{{ $pegawai->email }}" required />
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<x-input label="Nomor Telepon" name="no_telp" value="{{ $pegawai->no_telp }}" />
|
||||
<x-input label="Nomor Telepon" name="no_telp" type="tel" value="{{ $pegawai->no_telp }}" placeholder="0812-XXXX-XXXX" />
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<x-input label="Sisa Cuti (Hari)" name="sisa_cuti" type="number" value="{{ $pegawai->sisa_cuti }}"
|
||||
|
|
@ -49,7 +49,7 @@ class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|||
<div class="bg-white p-6 rounded-2xl shadow-sm border border-slate-200 block">
|
||||
<h3 class="text-lg font-bold text-slate-800 mb-4 block w-full">Penempatan & Status</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 w-full">
|
||||
<x-select label="Divisi" name="id_divisi" required>
|
||||
<x-select label="Divisi" name="id_divisi" class="!mb-0" required>
|
||||
@foreach ($divisi as $d)
|
||||
<option value="{{ $d->id_divisi }}"
|
||||
{{ $pegawai->id_divisi == $d->id_divisi ? 'selected' : '' }}>
|
||||
|
|
@ -57,7 +57,7 @@ class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|||
@endforeach
|
||||
</x-select>
|
||||
|
||||
<x-select label="Jabatan" name="id_jabatan" required>
|
||||
<x-select label="Jabatan" name="id_jabatan" class="!mb-0" required>
|
||||
@foreach ($jabatan as $j)
|
||||
<option value="{{ $j->id_jabatan }}"
|
||||
{{ $pegawai->id_jabatan == $j->id_jabatan ? 'selected' : '' }}>{{ $j->nama_jabatan }}
|
||||
|
|
@ -65,7 +65,7 @@ class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|||
@endforeach
|
||||
</x-select>
|
||||
|
||||
<x-select label="Lokasi Kantor" name="id_kantor" required>
|
||||
<x-select label="Lokasi Kantor" name="id_kantor" class="!mb-0" required>
|
||||
@foreach ($kantor as $k)
|
||||
<option value="{{ $k->id_kantor }}"
|
||||
{{ $pegawai->id_kantor == $k->id_kantor ? 'selected' : '' }}>
|
||||
|
|
@ -73,12 +73,12 @@ class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|||
@endforeach
|
||||
</x-select>
|
||||
|
||||
<x-select label="Status Pegawai" name="status_aktif" required>
|
||||
<x-select label="Status Pegawai" name="status_aktif" class="!mb-0" required>
|
||||
<option value="1" {{ $pegawai->status_aktif == 1 ? 'selected' : '' }}>Aktif</option>
|
||||
<option value="0" {{ $pegawai->status_aktif == 0 ? 'selected' : '' }}>Non-Aktif</option>
|
||||
</x-select>
|
||||
|
||||
<x-select label="Role Akses" name="id_role" required>
|
||||
<x-select label="Role Akses" name="id_role" class="!mb-0" required>
|
||||
@foreach ($roles as $r)
|
||||
<option value="{{ $r->id_role }}"
|
||||
{{ $pegawai->roles->contains('id_role', $r->id_role) ? 'selected' : '' }}>
|
||||
|
|
|
|||
|
|
@ -254,12 +254,12 @@ class="inline-flex py-1 px-3 rounded-full text-xs font-bold bg-red-100 text-red-
|
|||
{{ \Carbon\Carbon::parse($presensi->tanggal)->translatedFormat('d M Y') }}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-slate-600 text-center whitespace-nowrap">
|
||||
{{ $presensi->jadwal->shift->nama_shift ?? 'Shift Default' }}<br>
|
||||
<span class="text-xs text-slate-400">{{ substr($presensi->jadwal->shift->jam_mulai ?? '08:00', 0, 5) }} - {{ substr($presensi->jadwal->shift->jam_selesai ?? '17:00', 0, 5) }}</span>
|
||||
{{ $presensi->jadwal?->shift?->nama_shift ?? 'Shift Default' }}<br>
|
||||
<span class="text-xs text-slate-400">{{ substr($presensi->jadwal?->shift?->jam_mulai ?? '08:00', 0, 5) }} - {{ substr($presensi->jadwal?->shift?->jam_selesai ?? '17:00', 0, 5) }}</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-center whitespace-nowrap">
|
||||
<div class="flex flex-col text-sm items-center justify-center">
|
||||
<span class="font-mono font-bold {{ $presensi->jam_masuk > ($presensi->jadwal->shift->jam_mulai ?? '08:00') ? 'text-red-600' : 'text-emerald-600' }}">
|
||||
<span class="font-mono font-bold {{ $presensi->jam_masuk > ($presensi->jadwal?->shift?->jam_mulai ?? '08:00') ? 'text-red-600' : 'text-emerald-600' }}">
|
||||
{{ $presensi->jam_masuk ? \Carbon\Carbon::parse($presensi->jam_masuk)->format('H:i') : '--:--' }}
|
||||
</span>
|
||||
<span class="text-xs text-slate-400 font-mono mt-0.5">s/d</span>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@extends('layouts.app')
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Penggunaan Poin')
|
||||
|
||||
|
|
@ -20,33 +20,33 @@
|
|||
</x-slot:header>
|
||||
|
||||
@forelse ($penggunaan as $index => $item)
|
||||
<tr class="hover:bg-gray-50 transition-colors">
|
||||
<td class="px-6 py-4 text-sm text-gray-700">
|
||||
<tr class="hover:bg-slate-50 transition-colors">
|
||||
<td class="px-6 py-4 text-sm text-slate-700">
|
||||
{{ $penggunaan->firstItem() + $index }}
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="text-sm font-medium text-gray-900">{{ $item->user->nama_lengkap ?? '-' }}</div>
|
||||
<div class="text-xs text-gray-500">{{ $item->user->divisi->nama_divisi ?? '' }}</div>
|
||||
<div class="text-sm font-medium text-slate-900">{{ $item->user->nama_lengkap ?? '-' }}</div>
|
||||
<div class="text-xs text-slate-500">{{ $item->user->divisi->nama_divisi ?? '' }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-700">
|
||||
<td class="px-6 py-4 text-sm text-slate-700">
|
||||
{{ $item->tanggal_penggunaan ? $item->tanggal_penggunaan->translatedFormat('d F Y') : '-' }}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-700">
|
||||
<td class="px-6 py-4 text-sm text-slate-700">
|
||||
{{ $item->jenisPengurangan->nama_pengurangan ?? 'Lainnya' }}
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="font-bold text-amber-600">{{ $item->jumlah_poin }} Poin</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500">
|
||||
<td class="px-6 py-4 text-sm text-slate-500">
|
||||
{{ $item->tanggal_diajukan ? $item->tanggal_diajukan->translatedFormat('d M Y H:i') : '-' }}
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
@php
|
||||
$idStatus = $item->id_status;
|
||||
$badgeColor = match ($idStatus) {
|
||||
1 => 'yellow',
|
||||
2 => 'green',
|
||||
3 => 'red',
|
||||
\App\Enums\StatusPengajuan::PENDING => 'yellow',
|
||||
\App\Enums\StatusPengajuan::DISETUJUI => 'green',
|
||||
\App\Enums\StatusPengajuan::DITOLAK => 'red',
|
||||
default => 'gray'
|
||||
};
|
||||
@endphp
|
||||
|
|
@ -55,9 +55,9 @@
|
|||
</x-badge>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
@if($item->id_status == 1)
|
||||
@if($item->id_status == \App\Enums\StatusPengajuan::PENDING)
|
||||
<div class="flex justify-end gap-2">
|
||||
|
||||
|
||||
<form action="{{ route('penggunaan-poin.update', $item->id_penggunaan) }}" method="POST">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
|
@ -65,10 +65,7 @@
|
|||
<button type="submit"
|
||||
class="text-green-600 hover:text-green-900 bg-green-50 hover:bg-green-100 p-2 rounded-lg transition-all"
|
||||
title="Setujui">
|
||||
<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="M5 13l4 4L19 7"></path>
|
||||
</svg>
|
||||
<span class="text-xs font-semibold px-1">Setuju</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
|
|
@ -76,14 +73,11 @@ class="text-green-600 hover:text-green-900 bg-green-50 hover:bg-green-100 p-2 ro
|
|||
onclick="openRejectModal('{{ route('penggunaan-poin.update', $item->id_penggunaan) }}')"
|
||||
class="text-red-600 hover:text-red-900 bg-red-50 hover:bg-red-100 p-2 rounded-lg transition-all"
|
||||
title="Tolak">
|
||||
<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="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
<span class="text-xs font-semibold px-1">Tolak</span>
|
||||
</button>
|
||||
</div>
|
||||
@else
|
||||
<span class="text-xs text-gray-400 italic">Selesai</span>
|
||||
<span class="text-xs text-slate-400 italic">Selesai</span>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -106,7 +100,7 @@ class="text-red-600 hover:text-red-900 bg-red-50 hover:bg-red-100 p-2 rounded-lg
|
|||
placeholder="Jelaskan alasan kenapa pengajuan ini ditolak..." required rows="3" />
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-3 pt-4 border-t border-gray-100 mt-4">
|
||||
<div class="flex justify-end gap-3 pt-4 border-t border-slate-100 mt-4">
|
||||
<x-button type="button" variant="secondary" x-data
|
||||
@click="$dispatch('close-modal', 'reject-modal')">Batal</x-button>
|
||||
<x-button type="submit" class="bg-red-600 hover:bg-red-700 text-white">Tolak Pengajuan</x-button>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Buat Pengumuman')
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<x-page-header title="Buat Pengumuman Baru" subtitle="Isi form berikut untuk membuat pengumuman" />
|
||||
|
||||
<div class="mt-6 bg-white rounded-xl shadow-sm border border-gray-100 p-6">
|
||||
<div class="mt-6 bg-white rounded-xl shadow-sm border border-slate-200 p-6">
|
||||
<form action="{{ route('pengumuman.store') }}" method="POST" enctype="multipart/form-data">
|
||||
@csrf
|
||||
<div class="space-y-5">
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Edit Pengumuman')
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<x-page-header title="Edit Pengumuman" subtitle="Perbarui informasi pengumuman" />
|
||||
|
||||
<div class="mt-6 bg-white rounded-xl shadow-sm border border-gray-100 p-6">
|
||||
<div class="mt-6 bg-white rounded-xl shadow-sm border border-slate-200 p-6">
|
||||
<form action="{{ route('pengumuman.update', $pengumuman->id_pengumuman) }}" method="POST" enctype="multipart/form-data">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
|
|
|||
|
|
@ -23,13 +23,13 @@
|
|||
<th class="px-6 py-5 text-xs font-bold text-slate-500 uppercase tracking-wider text-right">Aksi</th>
|
||||
</x-slot:header>
|
||||
@forelse ($pengumuman as $item)
|
||||
<tr class="hover:bg-gray-50 transition-colors">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
<tr class="hover:bg-slate-50 transition-colors">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-500">
|
||||
{{ $loop->iteration + ($pengumuman->currentPage() - 1) * $pengumuman->perPage() }}
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="text-sm font-medium text-gray-900">{{ $item->judul }}</div>
|
||||
<div class="text-sm text-gray-500 truncate max-w-xs">{{ Str::limit($item->isi, 50) }}</div>
|
||||
<div class="text-sm font-medium text-slate-900">{{ $item->judul }}</div>
|
||||
<div class="text-sm text-slate-500 truncate max-w-xs">{{ Str::limit($item->isi, 50) }}</div>
|
||||
@if($item->foto || $item->lampiran)
|
||||
<div class="flex gap-2 mt-1">
|
||||
@if($item->foto)
|
||||
|
|
@ -47,10 +47,10 @@
|
|||
</div>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ $item->tanggal->format('d M Y') }}
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-500">
|
||||
{{ $item->tanggal ? $item->tanggal->translatedFormat('d M Y') : '-' }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-500">
|
||||
{{ $item->pembuat->nama_lengkap ?? 'System' }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
@extends('layouts.app')
|
||||
|
||||
@php use App\Enums\StatusValidasi; @endphp
|
||||
|
||||
@section('title', 'Monitoring Presensi')
|
||||
|
||||
@section('style')
|
||||
|
|
@ -24,7 +26,7 @@
|
|||
|
||||
<x-page-header title="Monitoring Presensi" subtitle="Pantau kehadiran pegawai harian.">
|
||||
<div class="flex gap-2">
|
||||
@if(Auth::user()->roles->contains(fn($role) => strtolower($role->nama_role) === 'hrd'))
|
||||
@if(Auth::user()->isGlobalAdmin())
|
||||
<a href="{{ route('presensi.create') }}" class="inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-lg text-white bg-slate-800 hover:bg-slate-900 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-slate-900 transition-colors shadow-sm text-sm h-[42px]">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
|
||||
|
|
@ -106,9 +108,9 @@ class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-bold
|
|||
</x-slot:header>
|
||||
|
||||
@forelse($presensi as $p)
|
||||
<tr class="hover:bg-slate-50 border-b border-slate-50 last:border-b-0 {{ $p->id_validasi == 2 ? 'bg-amber-50/50' : '' }}">
|
||||
<tr class="hover:bg-slate-50 border-b border-slate-50 last:border-b-0 {{ $p->id_validasi == StatusValidasi::PENDING ? 'bg-amber-50/50' : '' }}">
|
||||
<td class="px-6 py-4">
|
||||
@if($p->id_validasi == 2)
|
||||
@if($p->id_validasi == StatusValidasi::PENDING)
|
||||
<x-checkbox name="presensi_ids[]" value="{{ $p->id_presensi }}" class="presensi-checkbox" />
|
||||
@endif
|
||||
</td>
|
||||
|
|
@ -203,7 +205,7 @@ class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-bold
|
|||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
@if ($p->id_validasi == 2)
|
||||
@if ($p->id_validasi == StatusValidasi::PENDING)
|
||||
<button type="button" onclick="inlineApprove({{ $p->id_presensi }})" class="p-1.5 text-emerald-600 hover:text-emerald-800 bg-emerald-50 hover:bg-emerald-100 rounded-lg transition-colors border border-emerald-100" title="Setujui">
|
||||
<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="M5 13l4 4L19 7"></path></svg>
|
||||
</button>
|
||||
|
|
@ -212,7 +214,7 @@ class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-bold
|
|||
</button>
|
||||
@endif
|
||||
|
||||
@if(Auth::user()->roles->contains(fn($role) => strtolower($role->nama_role) === 'hrd'))
|
||||
@if(Auth::user()->isGlobalAdmin())
|
||||
<a href="{{ route('presensi.edit', $p->id_presensi) }}" class="p-1.5 text-blue-600 hover:text-blue-800 bg-blue-50 hover:bg-blue-100 rounded-lg transition-colors border border-blue-100" title="Koreksi Jam/Status">
|
||||
<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="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"></path></svg>
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,40 @@
|
|||
|
||||
@section('title', 'Pengaturan Akun')
|
||||
|
||||
@section('style')
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.6.2/cropper.min.css">
|
||||
<style>
|
||||
.cropper-view-box,
|
||||
.cropper-face {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.cropper-view-box {
|
||||
outline: 3px solid rgba(255, 255, 255, 0.8);
|
||||
outline-offset: 0;
|
||||
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.cropper-face {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.cropper-dashed,
|
||||
.cropper-point,
|
||||
.cropper-line {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.cropper-crop-box {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.cropper-modal {
|
||||
background-color: rgba(15, 23, 42, 0.85);
|
||||
}
|
||||
</style>
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
<div class="max-w-3xl mx-auto space-y-6">
|
||||
|
||||
|
|
@ -49,7 +83,7 @@ class="absolute bottom-0 right-0 p-2 bg-white rounded-full shadow-lg border bord
|
|||
</div>
|
||||
<div class="text-center">
|
||||
<h3 class="font-bold text-slate-800">{{ auth()->user()->nama_lengkap }}</h3>
|
||||
<p class="text-sm text-slate-500">{{ auth()->user()->role->nama_role ?? 'User' }}</p>
|
||||
<p class="text-sm text-slate-500">{{ auth()->user()->roles->first()->nama_role ?? 'User' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -74,7 +108,7 @@ class="absolute bottom-0 right-0 p-2 bg-white rounded-full shadow-lg border bord
|
|||
</div>
|
||||
|
||||
<div class="flex justify-end pt-6 border-t border-slate-100">
|
||||
<button type="submit"
|
||||
<button type="submit" id="submitBtn"
|
||||
class="px-6 py-2.5 bg-primary text-white font-semibold rounded-xl hover:bg-primary/90 transition shadow-lg shadow-indigo-500/20">
|
||||
Simpan Perubahan
|
||||
</button>
|
||||
|
|
@ -165,41 +199,9 @@ class="inline-flex items-center px-5 py-2.5 text-sm font-semibold text-white bg-
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
{{-- Cropper.js CDN --}}
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.6.2/cropper.min.css">
|
||||
@section('script')
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.6.2/cropper.min.js"></script>
|
||||
<script src="{{ asset('js/image-cropper.js') }}"></script>
|
||||
|
||||
{{-- Cropper circular overlay --}}
|
||||
<style>
|
||||
.cropper-view-box,
|
||||
.cropper-face {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.cropper-view-box {
|
||||
outline: 3px solid rgba(255, 255, 255, 0.8);
|
||||
outline-offset: 0;
|
||||
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.cropper-face {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.cropper-dashed,
|
||||
.cropper-point,
|
||||
.cropper-line {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.cropper-crop-box {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.cropper-modal {
|
||||
background-color: rgba(15, 23, 42, 0.85);
|
||||
}
|
||||
</style>
|
||||
@endsection
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ class="flex items-center gap-2 px-4 py-2 bg-primary text-white rounded-lg hover:
|
|||
|
||||
@forelse($roles as $index => $role)
|
||||
@php
|
||||
$roleKritis = in_array(strtolower($role->nama_role), ['manajer', 'manager', 'supervisor', 'hrd', 'super_admin', 'staff']);
|
||||
$roleKritis = in_array(strtolower($role->nama_role), \App\Http\Controllers\RoleController::ROLE_KRITIS);
|
||||
@endphp
|
||||
<tr class="hover:bg-slate-50 transition border-b border-slate-50 last:border-b-0">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-600">{{ $roles->firstItem() + $index }}</td>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@extends('layouts.app')
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Detail Surat Izin')
|
||||
|
||||
|
|
@ -191,29 +191,25 @@
|
|||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@section('style')
|
||||
<style>
|
||||
@media print {
|
||||
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
/* Sembunyikan elemen layout umum (sidebar & header) */
|
||||
aside, header {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Reset constraint agar browser dapat memproses multi-halaman jika suratnya panjang */
|
||||
body, html, #main-scroll-container, main, .h-screen, .overflow-hidden, .overflow-y-auto {
|
||||
height: auto !important;
|
||||
min-height: auto !important;
|
||||
overflow: visible !important;
|
||||
background-color: transparent !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
position: static !important;
|
||||
}
|
||||
|
||||
@page { margin: 1.5cm; }
|
||||
}
|
||||
</style>
|
||||
@endsection
|
||||
@endsection
|
||||
|
||||
@section('style')
|
||||
<style>
|
||||
@media print {
|
||||
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
aside, header {
|
||||
display: none !important;
|
||||
}
|
||||
body, html, #main-scroll-container, main, .h-screen, .overflow-hidden, .overflow-y-auto {
|
||||
height: auto !important;
|
||||
min-height: auto !important;
|
||||
overflow: visible !important;
|
||||
background-color: transparent !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
position: static !important;
|
||||
}
|
||||
@page { margin: 1.5cm; }
|
||||
}
|
||||
</style>
|
||||
@endsection
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@extends('layouts.app')
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Tukar Shift Kerja')
|
||||
|
||||
|
|
@ -190,7 +190,7 @@ function initTukarShiftForm() {
|
|||
form.addEventListener('submit', function (e) {
|
||||
if (user1Select.value === user2Select.value && user1Select.value !== '') {
|
||||
e.preventDefault();
|
||||
alert('Pegawai pertama dan kedua tidak boleh orang yang sama!');
|
||||
Swal.fire('Peringatan', 'Pegawai pertama dan kedua tidak boleh orang yang sama!', 'warning');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -280,7 +280,7 @@ function filterAllPegawaiByKantor() {
|
|||
} catch (error) {
|
||||
console.error('Error fetching schedules:', error);
|
||||
targetSelect.innerHTML = '<option value="">Gagal mengambil data</option>';
|
||||
alert('Terjadi kesalahan saat mengambil jadwal pegawai: ' + error.message);
|
||||
Swal.fire('Gagal', 'Terjadi kesalahan saat mengambil jadwal pegawai: ' + error.message, 'error');
|
||||
} finally {
|
||||
if (loadingEl) { loadingEl.classList.add('hidden'); }
|
||||
if (arrowEl) { arrowEl.classList.remove('hidden'); }
|
||||
|
|
@ -319,5 +319,6 @@ function filterAllPegawaiByKantor() {
|
|||
document.addEventListener('DOMContentLoaded', initTukarShiftForm);
|
||||
}
|
||||
</script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
<script src="{{ asset('js/form-handler.js') }}"></script>
|
||||
@endsection
|
||||
|
|
|
|||
Loading…
Reference in New Issue