diff --git a/app/Http/Controllers/Admin/Manage/AttendanceController.php b/app/Http/Controllers/Admin/Manage/AttendanceController.php new file mode 100644 index 0000000..c551702 --- /dev/null +++ b/app/Http/Controllers/Admin/Manage/AttendanceController.php @@ -0,0 +1,101 @@ +ajax()) { + $attendances = Attendance::query(); + + return DataTables::eloquent($attendances) + ->addIndexColumn() + ->addColumn('working_time', function ($item) { + if ($item->check_in) { + $checkIn = Carbon::parse($item->check_in); + + $checkOut = $item->check_out ? Carbon::parse($item->check_out) : Carbon::today()->endOfDay(); // Akhir hari: 23:59 + + $diff = $checkIn->diff($checkOut); + + if ($diff->h > 0) { + return $diff->format('%h jam %i menit'); + } else { + return $diff->format('%i menit'); + } + } + + return '-'; + }) + ->addColumn('check_in_image', function ($item) { + if ($item->check_in) { + $imageUrl = Storage::url("attendances/{$item->attachments->first()->formatted_attachment}"); + + return 'Check-in Image'; + } + + return '-'; + }) + ->addColumn('check_out_image', function ($item) { + if ($item->check_out) { + $imageUrl = Storage::url("attendances/{$item->attachments()->latest()->first()->formatted_attachment}"); + + return 'Check-out Image'; + } + + return '-'; + }) + ->rawColumns(['check_in_image', 'check_out_image']) + ->make(); + } + + return view('pages.admin.manage.attendances', [ + 'pageTitle' => 'Kehadiran', + 'checkInAttendance' => Attendance::where('user_id', Auth::id())->whereDate('created_at', today())->first(), + 'checkOutAttendance' => Attendance::where('user_id', Auth::id())->whereDate('created_at', today())->whereNotNull('check_out')->first(), + ]); + } + + public function store(AttendanceRequest $request): JsonResponse + { + $validatedData = $request->validated(); + $checkInAttendance = Attendance::where('user_id', Auth::id())->whereDate('created_at', today())->first(); + DB::transaction(function () use ($validatedData, $checkInAttendance) { + if (! $checkInAttendance) { + $newAttendance = Attendance::create([ + 'user_id' => Auth::id(), + 'barbershop_id' => Auth::user()->barbershop_id, + 'check_in' => now(), + ]); + } else { + $newAttendance = $checkInAttendance; + $newAttendance->update([ + 'check_out' => now(), + ]); + } + + $this->uploadAttachment($validatedData['image'], 'attendances', Attendance::class, $newAttendance->id); + }); + + return response()->json([ + 'status' => 'success', + 'message' => 'Image saved successfully!', + ], 200); + } +} diff --git a/app/Http/Requests/Admin/Manage/AttendanceRequest.php b/app/Http/Requests/Admin/Manage/AttendanceRequest.php new file mode 100644 index 0000000..79db125 --- /dev/null +++ b/app/Http/Requests/Admin/Manage/AttendanceRequest.php @@ -0,0 +1,33 @@ + ['required', function ($attribute, $value, $fail) { + if (! preg_match('/^data:image\/(\w+);base64,/', $value)) { + $fail('The '.$attribute.' is not a valid image.'); + } else { + $imageData = explode(',', $value)[1]; + $image = base64_decode($imageData); + + $imageInfo = @getimagesizefromstring($image); + if ($imageInfo === false) { + $fail('The '.$attribute.' is not a valid image.'); + } + } + }], + ]; + } +} diff --git a/app/Models/Attendance.php b/app/Models/Attendance.php index 683884d..2e9c97e 100644 --- a/app/Models/Attendance.php +++ b/app/Models/Attendance.php @@ -19,6 +19,10 @@ class Attendance extends Model 'check_out', ]; + protected $with = [ + 'attachments', + ]; + public function user(): BelongsTo { return $this->belongsTo(User::class); diff --git a/app/Traits/UploadAttachment.php b/app/Traits/UploadAttachment.php index b1eb65b..2717c92 100644 --- a/app/Traits/UploadAttachment.php +++ b/app/Traits/UploadAttachment.php @@ -10,32 +10,51 @@ trait UploadAttachment { - public function uploadAttachment(UploadedFile $file, string $path, string $classModel, string $featureId): void + public function uploadAttachment(UploadedFile|string $file, string $path, string $classModel, string $featureId): void { DB::transaction(function () use ($file, $path, $classModel, $featureId) { - $existingAttachment = Attachment::where('attachmentable_id', $featureId) - ->where('attachmentable_type', $classModel) - ->first(); + if (is_string($file) && strpos($file, 'data:image') === 0) { + $imageData = explode(',', $file)[1]; + $image = base64_decode($imageData); + $extension = 'png'; + $fileName = Str::uuid(); + $fullFileName = $fileName.'.'.$extension; - if ($existingAttachment) { - $oldFilePath = $path.'/'.$existingAttachment->created_at->format('Y-m-d').'/'.$existingAttachment->name.'.'.$existingAttachment->extension; - Storage::disk('public')->delete($oldFilePath); + $existingAttachment = Attachment::where('attachmentable_id', $featureId) + ->where('attachmentable_type', $classModel) + ->first(); - $existingAttachment->delete(); + if ($existingAttachment) { + if ($classModel !== 'App\\Models\\Attendance') { + $oldFilePath = $path.'/'.$existingAttachment->created_at->format('Y-m-d').'/'.$existingAttachment->name.'.'.$existingAttachment->extension; + Storage::disk('public')->delete($oldFilePath); + } + } + + $date = now()->format('Y-m-d'); + Storage::disk('public')->put($path.'/'.$date.'/'.$fullFileName, $image); + + Attachment::create([ + 'attachmentable_id' => $featureId, + 'attachmentable_type' => $classModel, + 'name' => $fileName, + 'extension' => $extension, + 'size' => strlen($image), + ]); + } elseif ($file instanceof UploadedFile) { + $date = now()->format('Y-m-d'); + $extension = $file->getClientOriginalExtension(); + $fileName = Str::uuid(); + $file->storeAs($path.'/'.$date, $fileName.'.'.$extension, 'public'); + + Attachment::create([ + 'attachmentable_id' => $featureId, + 'attachmentable_type' => $classModel, + 'name' => $fileName, + 'extension' => $extension, + 'size' => $file->getSize(), + ]); } - - $date = now()->format('Y-m-d'); - $extension = $file->getClientOriginalExtension(); - $fileName = Str::uuid(); - $file->storeAs($path.'/'.$date, $fileName.'.'.$extension, 'public'); - - Attachment::create([ - 'attachmentable_id' => $featureId, - 'attachmentable_type' => $classModel, - 'name' => $fileName, - 'extension' => $extension, - 'size' => $file->getSize(), - ]); }); } } diff --git a/public/assets/admin/css/style.css b/public/assets/admin/css/style.css index e57a08c..c1cbac1 100644 --- a/public/assets/admin/css/style.css +++ b/public/assets/admin/css/style.css @@ -10744,8 +10744,8 @@ @media (max-width: 1199px) { max-width: 100%; margin: 0 auto; -webkit-line-clamp: 3; - -webkit-box-orient: vertical; - overflow: hidden; + /* -webkit-box-orient: vertical; */ + /* overflow: hidden; */ text-overflow: ellipsis; } } diff --git a/public/assets/admin/js/custom/attendance.js b/public/assets/admin/js/custom/attendance.js new file mode 100644 index 0000000..4cb2856 --- /dev/null +++ b/public/assets/admin/js/custom/attendance.js @@ -0,0 +1,116 @@ + +$(document).ready(function () { + let webcamStream; + let imageData; + + $('#attendance-btn').on('click', async function () { + try { + webcamStream = await navigator.mediaDevices.getUserMedia({ + video: true + }); + $('#webcam').prop('srcObject', webcamStream); + $('#webcam-container').removeClass('d-none'); + $('#webcam').removeClass('d-none'); + $('#btn-capture').removeClass('d-none'); + $('#canvas-preview').addClass('d-none'); + $('#action-buttons').addClass('d-none'); + } catch (error) { + Swal.fire({ + icon: 'error', + title: 'Webcam Error', + text: 'Tidak dapat mengakses webcam Anda. Silakan periksa izin kamera Anda.', + confirmButtonText: 'Tutup' + }); + } + }); + + $('#btn-capture').on('click', function () { + const canvas = document.createElement('canvas'); + const webcam = $('#webcam')[0]; + canvas.width = webcam.videoWidth; + canvas.height = webcam.videoHeight; + const context = canvas.getContext('2d'); + + context.translate(canvas.width, 0); + context.scale(-1, 1); + context.drawImage(webcam, 0, 0, canvas.width, canvas.height); + + imageData = canvas.toDataURL('image/png'); + + const previewCanvas = $('#canvas-preview')[0]; + const previewContext = previewCanvas.getContext('2d'); + previewCanvas.width = canvas.width; + previewCanvas.height = canvas.height; + previewContext.drawImage(canvas, 0, 0); + + $('#webcam').addClass('d-none'); + $('#btn-capture').addClass('d-none'); + + $('#canvas-preview').removeClass('d-none'); + $('#action-buttons').removeClass('d-none'); + + webcamStream.getTracks().forEach(track => track.stop()); + }); + + $('#btn-retry').on('click', async function () { + try { + webcamStream = await navigator.mediaDevices.getUserMedia({ + video: true + }); + $('#webcam').prop('srcObject', webcamStream); + $('#webcam').removeClass('d-none'); + $('#btn-capture').removeClass('d-none'); + $('#canvas-preview').addClass('d-none'); + $('#action-buttons').addClass('d-none'); + } catch (error) { + Swal.fire({ + icon: 'error', + title: 'Webcam Error', + text: 'Gagal membuka kembali webcam. Silakan coba lagi.', + confirmButtonText: 'Tutup' + }); + console.error(error); + } + }); + + $('#btn-save').on('click', function () { + if (!imageData) { + Swal.fire({ + icon: 'warning', + title: 'Tidak Ada Gambar yang Diambil', + text: 'Silakan ambil gambar sebelum disimpan.', + confirmButtonText: 'Tutup' + }); + return; + } + + $.ajax({ + url: '/dashboard/manage/attendance/store', + type: 'POST', + data: { + _token: $('meta[name="csrf-token"]').attr('content'), + image: imageData + }, + success: function (response) { + Swal.fire({ + icon: 'success', + title: 'Gambar Disimpan', + text: 'Gambar Anda telah berhasil disimpan!', + confirmButtonText: 'OK' + }).then(() => { + $('#webcam-container').addClass('d-none'); + window.location.reload(); + }); + }, + error: function (error) { + Swal.fire({ + icon: 'error', + title: 'Gagal Menyimpan', + text: 'Ada masalah saat menyimpan gambar Anda. Silakan coba lagi.', + confirmButtonText: 'Tutup' + }); + + } + }); + }); +}); diff --git a/public/assets/admin/js/datatable/client-side.js b/public/assets/admin/js/datatable/client-side.js index 4d6e02e..7896f25 100644 --- a/public/assets/admin/js/datatable/client-side.js +++ b/public/assets/admin/js/datatable/client-side.js @@ -1,8 +1,5 @@ (function ($) { $('#datatable').DataTable({ - // scrollX: true, - // scrollCollapse: true, - stateSave: true, language: { emptyTable: "Tidak ada data yang tersedia pada tabel ini.", processing: "Sedang memproses ...", diff --git a/public/assets/admin/js/datatable/custom-table/attendance.js b/public/assets/admin/js/datatable/custom-table/attendance.js new file mode 100644 index 0000000..aaba2e5 --- /dev/null +++ b/public/assets/admin/js/datatable/custom-table/attendance.js @@ -0,0 +1,12 @@ +window.columns = [ + { + data: 'DT_RowIndex', + orderable: false, + searchable: false + }, + { data: 'check_in' }, + { data: 'check_in_image' }, + { data: 'check_out' }, + { data: 'check_out_image' }, + { data: 'working_time' }, +] diff --git a/public/assets/admin/js/datatable/server-side.js b/public/assets/admin/js/datatable/server-side.js new file mode 100644 index 0000000..51f47eb --- /dev/null +++ b/public/assets/admin/js/datatable/server-side.js @@ -0,0 +1,24 @@ +(function ($) { + $('#datatable').DataTable({ + serverSide: true, + processing: true, + ajax: $('#datatable').data('ajax'), + language: { + emptyTable: "Tidak ada data yang tersedia pada tabel ini.", + processing: "Sedang memproses ...", + search: "", + searchPlaceholder: "Ketik untuk mencari", + lengthMenu: `Lihat
_MENU_
`, + zeroRecords: "Tidak ditemukan data yang sesuai", + info: "_START_ - _END_ dari _TOTAL_", + infoEmpty: "", + infoFiltered: "(total _MAX_)", + }, + columns: columns, + pageLength: 10, + lengthMenu: [ + [10, 25, 50, 100], + [10, 25, 50, 100] + ], + }); +})(jQuery); diff --git a/resources/views/pages/admin/manage/attendances.blade.php b/resources/views/pages/admin/manage/attendances.blade.php new file mode 100644 index 0000000..7fc32db --- /dev/null +++ b/resources/views/pages/admin/manage/attendances.blade.php @@ -0,0 +1,146 @@ +@extends('layouts.admin') +@section('styles') + + +@endsection +@section('app') +
+
+
+ + + + + +
+ + +
+
+
+
+
+
+ @if (!$checkInAttendance) + + @endif +
+
+ @if ($checkInAttendance) + + @endif + @if (!$checkInAttendance) +
+

+ Selamat pagi, semoga hari Anda penuh semangat! Jangan lupa untuk melakukan absensi + kehadiran masuk sebelum memulai aktivitas Anda hari ini. +

+
+ @else +
+

+ Terima kasih, Anda telah melakukan absensi kehadiran masuk pada pukul + {{ $checkInAttendance->check_in }}.Semoga hari Anda produktif +

+
+ @endif +
+
+
+
+ @if ($checkInAttendance) +
+
+ @if (!$checkOutAttendance) + + @endif +
+
+ @if ($checkOutAttendance) + + @endif + @if (!$checkOutAttendance) +
+

+ Jangan lupa untuk melakukan absensi kehadiran pulang sebelum meninggalkan + tempat kerja. +

+
+ @else +
+

+ Terima kasih, Anda telah melakukan absensi kehadiran pulang pada pukul + {{ $checkOutAttendance->check_out }}.Semoga istirahat Anda menyegarkan untuk + hari berikutnya. +

+
+ @endif +
+
+
+
+ @endif +
+
+
+
+ + + + + + + + + + + + + +
NoMasukFotoPulangFotoTotal Kerja
+
+
+
+
+
+
+@endsection +@section('afterScripts') + + + + + +@endsection diff --git a/resources/views/partials/admin/_meta.blade.php b/resources/views/partials/admin/_meta.blade.php index 4d469db..81fdd45 100644 --- a/resources/views/partials/admin/_meta.blade.php +++ b/resources/views/partials/admin/_meta.blade.php @@ -6,4 +6,5 @@ content="Nikmati pengalaman cukur rambut terbaik dengan aplikasi barbershop kami. Pesan layanan cukur, potong rambut, dan grooming dengan mudah dan cepat."> + {{ $pageTitle }} - Barbershop diff --git a/resources/views/partials/admin/_sidebar.blade.php b/resources/views/partials/admin/_sidebar.blade.php index a9399e7..8f47dda 100644 --- a/resources/views/partials/admin/_sidebar.blade.php +++ b/resources/views/partials/admin/_sidebar.blade.php @@ -71,7 +71,7 @@ @@ -108,6 +108,18 @@ Kas + +
diff --git a/routes/web.php b/routes/web.php index 5dc2997..b8986ca 100644 --- a/routes/web.php +++ b/routes/web.php @@ -5,6 +5,7 @@ use App\Http\Controllers\Admin\Finance\CashController; use App\Http\Controllers\Admin\Finance\ExpenseController; use App\Http\Controllers\Admin\Finance\PayrollController; +use App\Http\Controllers\Admin\Manage\AttendanceController; use App\Http\Controllers\Admin\Master\BarbershopController; use App\Http\Controllers\Admin\Master\CustomerController; use App\Http\Controllers\Admin\Master\ServiceController; @@ -88,4 +89,12 @@ Route::delete('delete/{cash}', [CashController::class, 'delete'])->name('finance.cash.delete'); }); }); + + Route::prefix('manage')->group(function () { + Route::get('/attendances', [AttendanceController::class, 'index'])->name('manage.attendances'); + Route::prefix('attendance')->group(function () { + Route::post('store', [AttendanceController::class, 'store'])->name('manage.attendance.store'); + Route::delete('delete/{attendance}', [AttendanceController::class, 'delete'])->name('manage.attendance.delete'); + }); + }); });