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 '';
+ }
+
+ return '-';
+ })
+ ->addColumn('check_out_image', function ($item) {
+ if ($item->check_out) {
+ $imageUrl = Storage::url("attendances/{$item->attachments()->latest()->first()->formatted_attachment}");
+
+ return '
';
+ }
+
+ 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
+ Selamat pagi, semoga hari Anda penuh semangat! Jangan lupa untuk melakukan absensi + kehadiran masuk sebelum memulai aktivitas Anda hari ini. +
++ Terima kasih, Anda telah melakukan absensi kehadiran masuk pada pukul + {{ $checkInAttendance->check_in }}.Semoga hari Anda produktif +
++ Jangan lupa untuk melakukan absensi kehadiran pulang sebelum meninggalkan + tempat kerja. +
++ Terima kasih, Anda telah melakukan absensi kehadiran pulang pada pukul + {{ $checkOutAttendance->check_out }}.Semoga istirahat Anda menyegarkan untuk + hari berikutnya. +
+| No | +Masuk | +Foto | +Pulang | +Foto | +Total Kerja | +
|---|