feat(attendance): crud,open cam js,datatable server side dan lainnya yang berhubungan dengan kehadiran

This commit is contained in:
Yoga Pangestu 2024-11-25 23:09:06 +07:00
parent a6200884ce
commit 048131b2bb
13 changed files with 501 additions and 27 deletions

View File

@ -0,0 +1,101 @@
<?php
namespace App\Http\Controllers\Admin\Manage;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\Manage\AttendanceRequest;
use App\Models\Attendance;
use App\Traits\UploadAttachment;
use Carbon\Carbon;
use Illuminate\Contracts\View\View;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Yajra\DataTables\Facades\DataTables;
class AttendanceController extends Controller
{
use UploadAttachment;
public function index(Request $request): View|JsonResponse
{
if ($request->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 '<a href="'.$imageUrl.'" ><img src="'.$imageUrl.'" alt="Check-in Image" height="77" width="77"></a>';
}
return '-';
})
->addColumn('check_out_image', function ($item) {
if ($item->check_out) {
$imageUrl = Storage::url("attendances/{$item->attachments()->latest()->first()->formatted_attachment}");
return '<a href="'.$imageUrl.'" ><img src="'.$imageUrl.'" alt="Check-out Image" height="77" width="77"></a>';
}
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);
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace App\Http\Requests\Admin\Manage;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
class AttendanceRequest extends FormRequest
{
public function authorize(): bool
{
return Auth::check();
}
public function rules(): array
{
return [
'image' => ['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.');
}
}
}],
];
}
}

View File

@ -19,6 +19,10 @@ class Attendance extends Model
'check_out',
];
protected $with = [
'attachments',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);

View File

@ -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(),
]);
});
}
}

View File

@ -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;
}
}

View File

@ -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'
});
}
});
});
});

View File

@ -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 ...",

View File

@ -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' },
]

View File

@ -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: `<span class="d-none d-sm-inline-block">Lihat</span><div class="form-control-select mt-2"> _MENU_ </div>`,
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);

View File

@ -0,0 +1,146 @@
@extends('layouts.admin')
@section('styles')
<link rel="stylesheet" type="text/css" href="{{ asset('assets/admin/css/vendors/datatables.css') }}">
<style>
#webcam-container {
margin-top: 20px;
text-align: center;
}
#webcam {
transform: scaleX(-1);
=-webkit-transform: scaleX(-1);
}
</style>
@endsection
@section('app')
<div class="container-fluid">
<div class="row d-flex justify-content-center">
<div class="col-sm-12 col-xl-6 d-none mb-3" id="webcam-container">
<video id="webcam" autoplay playsinline style="width: 100%;"></video>
<button id="btn-capture" class="btn btn-primary mt-3">Ambil</button>
<canvas id="canvas-preview" class="d-none" style="width: 100%; border: 1px solid #ccc;"></canvas>
<div id="action-buttons" class="d-none mt-3">
<button id="btn-retry" class="btn btn-warning">Ulangi</button>
<button id="btn-save" class="btn btn-success">Simpan</button>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12 col-xl-6">
<div class="card card-absolute">
@if (!$checkInAttendance)
<div class="card-header bg-primary">
<a href="javascript:void(0)" id="attendance-btn">
<h5 class="txt-light">Hadir</h5>
</a>
</div>
@endif
<div class="card-body">
<div class="d-flex list-behavior-1 align-items-center">
@if ($checkInAttendance)
<div class="flex-shrink-0">
<a
href="{{ Storage::url("attendances/{$checkInAttendance->attachments->first()->formatted_attachment}") }}">
<img class="tab-img img-fluid"
src="{{ Storage::url("attendances/{$checkInAttendance->attachments->first()->formatted_attachment}") }}"
alt="image">
</a>
</div>
@endif
@if (!$checkInAttendance)
<div class="flex-grow-1">
<p class="mb-xl-0 mb-sm-4">
Selamat pagi, semoga hari Anda penuh semangat! Jangan lupa untuk melakukan absensi
kehadiran masuk sebelum memulai aktivitas Anda hari ini.
</p>
</div>
@else
<div class="flex-grow-1">
<p class="mb-xl-0 mb-sm-4">
Terima kasih, Anda telah melakukan absensi kehadiran masuk pada pukul
{{ $checkInAttendance->check_in }}.Semoga hari Anda produktif
</p>
</div>
@endif
</div>
</div>
</div>
</div>
@if ($checkInAttendance)
<div class="col-sm-12 col-xl-6">
<div class="card card-absolute">
@if (!$checkOutAttendance)
<div class="card-header bg-primary">
<a href="javascript:void(0)" id="attendance-btn">
<h5 class="txt-light">Hadir</h5>
</a>
</div>
@endif
<div class="card-body">
<div class="d-flex list-behavior-1 align-items-center">
@if ($checkOutAttendance)
<div class="flex-shrink-0">
<a
href="{{ Storage::url("attendances/{$checkOutAttendance->attachments()->latest()->first()->formatted_attachment}") }}">
<img class="tab-img img-fluid"
src="{{ Storage::url("attendances/{$checkOutAttendance->attachments()->latest()->first()->formatted_attachment}") }}"
alt="image">
</a>
</div>
@endif
@if (!$checkOutAttendance)
<div class="flex-grow-1">
<p class="mb-xl-0 mb-sm-4">
Jangan lupa untuk melakukan absensi kehadiran pulang sebelum meninggalkan
tempat kerja.
</p>
</div>
@else
<div class="flex-grow-1">
<p class="mb-xl-0 mb-sm-4">
Terima kasih, Anda telah melakukan absensi kehadiran pulang pada pukul
{{ $checkOutAttendance->check_out }}.Semoga istirahat Anda menyegarkan untuk
hari berikutnya.
</p>
</div>
@endif
</div>
</div>
</div>
</div>
@endif
<div class="col-12">
<div class="card">
<div class="card-body">
<div class="table-responsive">
<table class="display" id="datatable" data-ajax="{{ url(url()->current()) }}">
<thead>
<tr>
<th>No</th>
<th>Masuk</th>
<th>Foto</th>
<th>Pulang</th>
<th>Foto</th>
<th>Total Kerja</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
@endsection
@section('afterScripts')
<script src="{{ asset('assets/admin/js/datatable/jquery.dataTables.min.js?ver=1.0.0') }}"></script>
<script src="{{ asset('assets/admin/js/datatable/custom-table/attendance.js?ver=1.0.0') }}"></script>
<script src="{{ asset('assets/admin/js/datatable/server-side.js?ver=1.0.0') }}"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script src="{{ asset('assets/admin/js/custom/attendance.js?ver=1.0.0') }}"></script>
@endsection

View File

@ -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.">
<meta name="keywords" content="barbershop, potong rambut, cukur rambut, booking barbershop, layanan grooming">
<meta name="author" content="Barbershop">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ $pageTitle }} - Barbershop</title>

View File

@ -71,7 +71,7 @@
<li class="sidebar-list">
<i class="fa fa-thumb-tack"></i>
<a class="sidebar-link sidebar-title link-nav" href="{{ route('master.customers') }}">
<i data-feather="user-check"></i>
<i data-feather="user-plus"></i>
<span>Pelanggan</span>
</a>
</li>
@ -108,6 +108,18 @@
<span>Kas</span>
</a>
</li>
<li class="sidebar-main-title">
<div>
<h6>Kelola</h6>
</div>
</li>
<li class="sidebar-list">
<i class="fa fa-thumb-tack"></i>
<a class="sidebar-link sidebar-title link-nav" href="{{ route('manage.attendances') }}">
<i data-feather="user-check"></i>
<span>Kehadiran</span>
</a>
</li>
</ul>
</div>
<div class="right-arrow" id="right-arrow"><i data-feather="arrow-right"></i></div>

View File

@ -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');
});
});
});