- Implemented push notifications with a service worker (sw.ts) to handle caching and notifications. - Added API routes for push subscription and notification management in routes/api.php. - Created NotificationTest to validate notification service functionality and ensure correct notifications are sent based on user roles and actions. - Updated Permissions component to manage notification permissions and subscriptions. - Enhanced CategoryIndex component to highlight categories based on notifications. - Excluded service worker from TypeScript compilation in tsconfig.json. - Updated Vite configuration to include service worker in the build process.
216 lines
7.3 KiB
PHP
216 lines
7.3 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Admin\HR;
|
|
|
|
use App\Models\Attendance;
|
|
use App\Models\LeaveRequest;
|
|
use App\Services\NotificationService;
|
|
use App\Services\S3PresignedService;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Str;
|
|
use Spatie\MediaLibrary\MediaCollections\Models\Media;
|
|
|
|
class AttendanceService
|
|
{
|
|
public function __construct(
|
|
private S3PresignedService $s3Service = new S3PresignedService,
|
|
) {}
|
|
|
|
public function getAll(): Collection
|
|
{
|
|
return Attendance::with(['employee.user.userProfile', 'media'])
|
|
->latest('attendance_date')
|
|
->get()
|
|
->map(fn (Attendance $attendance) => $this->formatAttendance($attendance));
|
|
}
|
|
|
|
public function getByMonth(int $year, int $month): Collection
|
|
{
|
|
return Attendance::with(['employee.user.userProfile', 'media'])
|
|
->whereYear('attendance_date', $year)
|
|
->whereMonth('attendance_date', $month)
|
|
->get()
|
|
->map(fn (Attendance $attendance) => $this->formatAttendance($attendance));
|
|
}
|
|
|
|
public function getByDate(string $date): ?array
|
|
{
|
|
$attendance = Attendance::with(['employee.user.userProfile', 'media'])
|
|
->where('attendance_date', $date)
|
|
->where('employee_id', auth()->user()->employee?->id)
|
|
->first();
|
|
|
|
return $attendance ? $this->formatAttendance($attendance) : null;
|
|
}
|
|
|
|
public function getToday(): ?array
|
|
{
|
|
return $this->getByDate(now()->toDateString());
|
|
}
|
|
|
|
public function getMonthStats(int $year, int $month): array
|
|
{
|
|
$startOfMonth = Carbon::createFromDate($year, $month, 1)->startOfMonth();
|
|
$endOfMonth = $startOfMonth->copy()->endOfMonth();
|
|
|
|
$workingDays = 0;
|
|
$current = $startOfMonth->copy();
|
|
while ($current->lte($endOfMonth)) {
|
|
if (! in_array($current->dayOfWeek, [Carbon::SATURDAY, Carbon::SUNDAY])) {
|
|
$workingDays++;
|
|
}
|
|
$current->addDay();
|
|
}
|
|
|
|
$attendanceCount = Attendance::whereYear('attendance_date', $year)
|
|
->whereMonth('attendance_date', $month)
|
|
->count();
|
|
|
|
$leaveDays = LeaveRequest::approved()
|
|
->where('start_date', '<=', $endOfMonth)
|
|
->where('end_date', '>=', $startOfMonth)
|
|
->get()
|
|
->reduce(function ($carry, $leave) use ($startOfMonth, $endOfMonth) {
|
|
$leaveStart = max($leave->start_date->timestamp, $startOfMonth->timestamp);
|
|
$leaveEnd = min($leave->end_date->timestamp, $endOfMonth->timestamp);
|
|
$days = Carbon::createFromTimestamp($leaveStart)->diffInDays(Carbon::createFromTimestamp($leaveEnd)) + 1;
|
|
|
|
return $carry + max(0, $days);
|
|
}, 0);
|
|
|
|
return [
|
|
'working_days' => $workingDays,
|
|
'present' => $attendanceCount,
|
|
'absent' => max(0, $workingDays - $attendanceCount - $leaveDays),
|
|
'leave' => $leaveDays,
|
|
];
|
|
}
|
|
|
|
public function checkIn(array $data): Attendance
|
|
{
|
|
$employee = auth()->user()->employee;
|
|
|
|
if (! $employee) {
|
|
throw new \Exception('Anda tidak terdaftar sebagai karyawan.');
|
|
}
|
|
|
|
$today = now()->toDateString();
|
|
|
|
$existing = Attendance::where('employee_id', $employee->id)
|
|
->where('attendance_date', $today)
|
|
->first();
|
|
|
|
if ($existing) {
|
|
throw new \Exception('Anda sudah melakukan presensi hari ini.');
|
|
}
|
|
|
|
$attendance = Attendance::create([
|
|
'employee_id' => $employee->id,
|
|
'attendance_date' => $today,
|
|
'check_in_at' => now(),
|
|
'check_in_latitude' => $data['latitude'],
|
|
'check_in_longitude' => $data['longitude'],
|
|
]);
|
|
|
|
if (! empty($data['photo'])) {
|
|
$this->registerMedia($attendance, $data['photo'], 'check-in');
|
|
}
|
|
|
|
NotificationService::notify(
|
|
roles: ['Owner', 'Developer', 'Direktur'],
|
|
title: 'Presensi Masuk',
|
|
body: 'Presensi masuk oleh '.auth()->user()->full_name.'.',
|
|
url: route('admin.hr.attendances.index'),
|
|
);
|
|
|
|
return $attendance;
|
|
}
|
|
|
|
public function checkOut(Attendance $attendance, array $data): Attendance
|
|
{
|
|
$attendance->update([
|
|
'check_out_at' => now(),
|
|
'check_out_latitude' => $data['latitude'],
|
|
'check_out_longitude' => $data['longitude'],
|
|
]);
|
|
|
|
if (! empty($data['photo'])) {
|
|
$this->registerMedia($attendance, $data['photo'], 'check-out');
|
|
}
|
|
|
|
NotificationService::notify(
|
|
roles: ['Owner', 'Developer', 'Direktur'],
|
|
title: 'Presensi Pulang',
|
|
body: 'Presensi pulang oleh '.auth()->user()->full_name.'.',
|
|
url: route('admin.hr.attendances.index'),
|
|
);
|
|
|
|
return $attendance;
|
|
}
|
|
|
|
private function formatAttendance(Attendance $attendance): array
|
|
{
|
|
$toArray = $attendance->toArray();
|
|
|
|
if (is_null($toArray['work_duration_minutes']) && $attendance->check_in_at) {
|
|
$checkIn = Carbon::parse($attendance->check_in_at);
|
|
$end = $checkIn->toDateString() === now()->toDateString()
|
|
? now()
|
|
: $checkIn->copy()->endOfDay();
|
|
$toArray['work_duration_minutes'] = $checkIn->diffInMinutes($end);
|
|
}
|
|
|
|
$checkInMedia = $attendance->getFirstMedia('check-in');
|
|
$checkOutMedia = $attendance->getFirstMedia('check-out');
|
|
|
|
$toArray['check_in_photo'] = $checkInMedia
|
|
? Cache::remember("attendance_check_in_{$checkInMedia->id}", now()->addMinutes(55), fn () => $this->s3Service->getTemporaryUrl($checkInMedia->file_name))
|
|
: null;
|
|
|
|
$toArray['check_out_photo'] = $checkOutMedia
|
|
? Cache::remember("attendance_check_out_{$checkOutMedia->id}", now()->addMinutes(55), fn () => $this->s3Service->getTemporaryUrl($checkOutMedia->file_name))
|
|
: null;
|
|
|
|
return $toArray;
|
|
}
|
|
|
|
private function registerMedia(Attendance $attendance, string $photo, string $type): void
|
|
{
|
|
if (str_starts_with($photo, 'data:image')) {
|
|
$base64 = explode(',', $photo)[1];
|
|
$imageData = base64_decode($base64);
|
|
$filename = $type.'_'.time().'_'.uniqid().'.jpg';
|
|
$s3Key = 'attendances/'.$filename;
|
|
|
|
app('filesystem')->disk('s3')->put($s3Key, $imageData);
|
|
$mimeType = 'image/jpeg';
|
|
$fileSize = strlen($imageData);
|
|
} else {
|
|
$s3Key = $photo;
|
|
$filename = pathinfo($photo, PATHINFO_BASENAME);
|
|
$mimeType = 'image/jpeg';
|
|
$fileSize = 0;
|
|
}
|
|
|
|
Media::create([
|
|
'model_type' => Attendance::class,
|
|
'model_id' => $attendance->id,
|
|
'uuid' => Str::uuid(),
|
|
'collection_name' => $type,
|
|
'name' => $type,
|
|
'file_name' => $s3Key,
|
|
'mime_type' => $mimeType,
|
|
'disk' => 's3',
|
|
'conversions_disk' => 's3',
|
|
'size' => $fileSize,
|
|
'manipulations' => [],
|
|
'custom_properties' => [],
|
|
'generated_conversions' => [],
|
|
'responsive_images' => [],
|
|
'order_column' => 1,
|
|
]);
|
|
}
|
|
}
|