feat: implement attendance management features including penalty application, reminders, and HR settings management
This commit is contained in:
parent
fe7a3e4813
commit
55b779c76e
178
app/Console/Commands/ApplyAttendancePenalties.php
Normal file
178
app/Console/Commands/ApplyAttendancePenalties.php
Normal file
@ -0,0 +1,178 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Enums\EmployeeStatus;
|
||||
use App\Enums\LeaveRequestStatus;
|
||||
use App\Enums\PayrollAdjustmentType;
|
||||
use App\Enums\PayrollPeriodStatus;
|
||||
use App\Models\Attendance;
|
||||
use App\Models\Employee;
|
||||
use App\Models\LeaveRequest;
|
||||
use App\Models\Payroll;
|
||||
use App\Models\PayrollPeriod;
|
||||
use App\Settings\HrSettings;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class ApplyAttendancePenalties extends Command
|
||||
{
|
||||
protected $signature = 'attendance:apply-penalties {--date= : Tanggal yang diproses (default: kemarin)}';
|
||||
|
||||
protected $description = 'Buat potongan gaji otomatis untuk keterlambatan dan bolos';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$date = $this->option('date')
|
||||
? Carbon::parse($this->option('date'))->startOfDay()
|
||||
: Carbon::yesterday()->startOfDay();
|
||||
|
||||
$settings = app(HrSettings::class);
|
||||
$latePenalty = $settings->late_penalty_amount;
|
||||
$absentPenalty = $settings->absent_penalty_amount;
|
||||
$scheduledTime = $settings->scheduled_check_in_time;
|
||||
|
||||
if ($latePenalty <= 0 && $absentPenalty <= 0) {
|
||||
$this->info('Denda keterlambatan dan bolos belum diatur (0). Lewatkan.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$scheduledCheckIn = Carbon::createFromFormat('Y-m-d H:i', $date->format('Y-m-d').' '.$scheduledTime);
|
||||
|
||||
$employees = Employee::query()
|
||||
->where('status', EmployeeStatus::ACTIVE)
|
||||
->get();
|
||||
|
||||
$lateCount = 0;
|
||||
$absentCount = 0;
|
||||
$skippedLeave = 0;
|
||||
$skippedDuplicate = 0;
|
||||
|
||||
foreach ($employees as $employee) {
|
||||
$hasLeave = LeaveRequest::query()
|
||||
->where('employee_id', $employee->id)
|
||||
->where('status', LeaveRequestStatus::APPROVED)
|
||||
->whereDate('start_date', '<=', $date)
|
||||
->whereDate('end_date', '>=', $date)
|
||||
->exists();
|
||||
|
||||
if ($hasLeave) {
|
||||
$skippedLeave++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$attendance = Attendance::query()
|
||||
->where('employee_id', $employee->id)
|
||||
->whereDate('attendance_date', $date)
|
||||
->first();
|
||||
|
||||
if ($attendance === null) {
|
||||
if ($absentPenalty <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$payroll = $this->findOpenPayroll($employee->id);
|
||||
|
||||
if ($payroll === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$alreadyExists = $payroll->adjustments()
|
||||
->where('attendance_id', null)
|
||||
->where('type', PayrollAdjustmentType::DEDUCTION)
|
||||
->where('description', 'like', "%Bolos {$date->format('d/m/Y')}%")
|
||||
->exists();
|
||||
|
||||
if ($alreadyExists) {
|
||||
$skippedDuplicate++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$payroll->adjustments()->create([
|
||||
'type' => PayrollAdjustmentType::DEDUCTION,
|
||||
'amount' => $absentPenalty,
|
||||
'description' => "Bolos {$date->format('d/m/Y')}",
|
||||
'created_by_id' => 1,
|
||||
]);
|
||||
|
||||
$payroll->load('adjustments');
|
||||
$payroll->recalculateAmounts();
|
||||
$payroll->save();
|
||||
|
||||
$absentCount++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($latePenalty <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$checkInTime = Carbon::parse($attendance->check_in_at);
|
||||
|
||||
if ($checkInTime->lte($scheduledCheckIn)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$payroll = $this->findOpenPayroll($employee->id);
|
||||
|
||||
if ($payroll === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$alreadyExists = $payroll->adjustments()
|
||||
->where('attendance_id', $attendance->id)
|
||||
->where('type', PayrollAdjustmentType::DEDUCTION)
|
||||
->exists();
|
||||
|
||||
if ($alreadyExists) {
|
||||
$skippedDuplicate++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$minutesLate = (int) $scheduledCheckIn->diffInMinutes($checkInTime);
|
||||
|
||||
$payroll->adjustments()->create([
|
||||
'type' => PayrollAdjustmentType::DEDUCTION,
|
||||
'amount' => $latePenalty,
|
||||
'description' => "Terlambat {$date->format('d/m/Y')} ({$minutesLate} menit)",
|
||||
'attendance_id' => $attendance->id,
|
||||
'created_by_id' => 1,
|
||||
]);
|
||||
|
||||
$payroll->load('adjustments');
|
||||
$payroll->recalculateAmounts();
|
||||
$payroll->save();
|
||||
|
||||
$lateCount++;
|
||||
}
|
||||
|
||||
$this->info("Selesai memproses tanggal {$date->format('d/m/Y')}:");
|
||||
$this->info(" - Terlambat: {$lateCount} pegawai");
|
||||
$this->info(" - Bolos: {$absentCount} pegawai");
|
||||
$this->info(" - Lewati (cuti): {$skippedLeave} pegawai");
|
||||
$this->info(" - Lewati (duplikat): {$skippedDuplicate} pegawai");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function findOpenPayroll(int $employeeId): ?Payroll
|
||||
{
|
||||
$period = PayrollPeriod::query()
|
||||
->where('status', PayrollPeriodStatus::OPEN)
|
||||
->first();
|
||||
|
||||
if ($period === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Payroll::query()
|
||||
->where('payroll_period_id', $period->id)
|
||||
->where('employee_id', $employeeId)
|
||||
->first();
|
||||
}
|
||||
}
|
||||
64
app/Console/Commands/SendAttendanceReminder.php
Normal file
64
app/Console/Commands/SendAttendanceReminder.php
Normal file
@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Enums\EmployeeStatus;
|
||||
use App\Models\Attendance;
|
||||
use App\Models\Employee;
|
||||
use App\Services\System\PushNotificationService;
|
||||
use App\Settings\HrSettings;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class SendAttendanceReminder extends Command
|
||||
{
|
||||
protected $signature = 'attendance:send-reminder';
|
||||
|
||||
protected $description = 'Kirim pengingat presensi ke pegawai 30 menit sebelum jam masuk';
|
||||
|
||||
public function handle(PushNotificationService $pushNotificationService): int
|
||||
{
|
||||
$settings = app(HrSettings::class);
|
||||
$scheduledTime = $settings->scheduled_check_in_time;
|
||||
|
||||
if ($scheduledTime === '00:00') {
|
||||
$this->info('Jam masuk kerja belum diatur. Lewatkan.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$now = Carbon::now();
|
||||
$scheduledCheckIn = Carbon::createFromFormat('Y-m-d H:i', $now->format('Y-m-d').' '.$scheduledTime);
|
||||
|
||||
$employees = Employee::query()
|
||||
->where('status', EmployeeStatus::ACTIVE)
|
||||
->whereNotNull('user_id')
|
||||
->get();
|
||||
|
||||
$sentCount = 0;
|
||||
|
||||
foreach ($employees as $employee) {
|
||||
$alreadyCheckedIn = Attendance::query()
|
||||
->where('employee_id', $employee->id)
|
||||
->whereDate('attendance_date', $now->toDateString())
|
||||
->exists();
|
||||
|
||||
if ($alreadyCheckedIn) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$pushNotificationService->sendToUser(
|
||||
'⏰ Pengingat Presensi',
|
||||
"Jangan lupa melakukan presensi masuk sebelum pukul {$scheduledTime}.",
|
||||
$employee->user_id,
|
||||
'/admin/hr/attendances',
|
||||
);
|
||||
|
||||
$sentCount++;
|
||||
}
|
||||
|
||||
$this->info("Pengingat presensi dikirim ke {$sentCount} pegawai.");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@ -44,6 +44,9 @@ public function index(Request $request): Response
|
||||
'todayAttendance' => $employee
|
||||
? $this->attendanceService->todayAttendanceForEmployee($employee)
|
||||
: null,
|
||||
'isOnLeave' => $employee
|
||||
? $this->attendanceService->isOnLeaveToday($employee)
|
||||
: false,
|
||||
'canCheckIn' => ($user?->can(Permission::ATTENDANCES_CREATE->value) ?? false)
|
||||
&& $employee !== null,
|
||||
'canManageAll' => $isManager,
|
||||
|
||||
@ -4,9 +4,11 @@
|
||||
|
||||
use App\Http\Controllers\Concerns\FlashesEntityMessage;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\System\Setting\HrSettingRequest;
|
||||
use App\Http\Requests\Admin\System\Setting\MarketplaceRequest;
|
||||
use App\Http\Requests\Admin\System\Setting\SocialMediaRequest;
|
||||
use App\Http\Requests\Admin\System\Setting\SystemRequest;
|
||||
use App\Services\System\Setting\HrSettingService;
|
||||
use App\Services\System\Setting\MarketplaceService;
|
||||
use App\Services\System\Setting\SocialMediaService;
|
||||
use App\Services\System\Setting\SystemService;
|
||||
@ -22,6 +24,7 @@ public function __construct(
|
||||
private readonly SystemService $systemService,
|
||||
private readonly SocialMediaService $socialMediaService,
|
||||
private readonly MarketplaceService $marketplaceService,
|
||||
private readonly HrSettingService $hrSettingService,
|
||||
) {}
|
||||
|
||||
public function index(): Response
|
||||
@ -30,6 +33,7 @@ public function index(): Response
|
||||
'system' => $this->systemService->systemData(),
|
||||
'socialMedia' => $this->socialMediaService->socialMediaData(),
|
||||
'marketplace' => $this->marketplaceService->marketplaceData(),
|
||||
'hr' => $this->hrSettingService->hrData(),
|
||||
]);
|
||||
}
|
||||
|
||||
@ -59,4 +63,13 @@ public function updateMarketplace(MarketplaceRequest $request): RedirectResponse
|
||||
|
||||
return redirect()->route('admin.system.setting.index');
|
||||
}
|
||||
|
||||
public function updateHr(HrSettingRequest $request): RedirectResponse
|
||||
{
|
||||
$this->hrSettingService->updateHr($request->validated());
|
||||
|
||||
$this->flashSuccess('Pengaturan HR berhasil disimpan.');
|
||||
|
||||
return redirect()->route('admin.system.setting.index');
|
||||
}
|
||||
}
|
||||
|
||||
40
app/Http/Requests/Admin/System/Setting/HrSettingRequest.php
Normal file
40
app/Http/Requests/Admin/System/Setting/HrSettingRequest.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\System\Setting;
|
||||
|
||||
use App\Enums\Permission;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class HrSettingRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()?->can(Permission::SETTINGS_UPDATE->value) ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'scheduled_check_in_time' => ['required', 'string', 'max:5', 'regex:/^\d{2}:\d{2}$/'],
|
||||
'scheduled_check_out_time' => ['required', 'string', 'max:5', 'regex:/^\d{2}:\d{2}$/'],
|
||||
'late_penalty_amount' => ['required', 'integer', 'min:0'],
|
||||
'absent_penalty_amount' => ['required', 'integer', 'min:0'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function attributes(): array
|
||||
{
|
||||
return [
|
||||
'scheduled_check_in_time' => 'jam masuk kerja',
|
||||
'scheduled_check_out_time' => 'jam pulang kerja',
|
||||
'late_penalty_amount' => 'denda keterlambatan',
|
||||
'absent_penalty_amount' => 'denda bolos',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -39,6 +39,11 @@ public function payroll(): BelongsTo
|
||||
return $this->belongsTo(Payroll::class);
|
||||
}
|
||||
|
||||
public function attendance(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Attendance::class);
|
||||
}
|
||||
|
||||
public function amountFormatted(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
|
||||
@ -2,8 +2,10 @@
|
||||
|
||||
namespace App\Services\Hr;
|
||||
|
||||
use App\Enums\LeaveRequestStatus;
|
||||
use App\Models\Attendance;
|
||||
use App\Models\Employee;
|
||||
use App\Models\LeaveRequest;
|
||||
use App\Models\User;
|
||||
use App\Services\Concerns\ResolvesAuthEmployee;
|
||||
use App\Services\Media\MediaService;
|
||||
@ -58,6 +60,16 @@ public function todayAttendanceForEmployee(Employee $employee): ?array
|
||||
return $attendance?->toArray();
|
||||
}
|
||||
|
||||
public function isOnLeaveToday(Employee $employee): bool
|
||||
{
|
||||
return LeaveRequest::query()
|
||||
->where('employee_id', $employee->id)
|
||||
->where('status', LeaveRequestStatus::APPROVED)
|
||||
->whereDate('start_date', '<=', today())
|
||||
->whereDate('end_date', '>=', today())
|
||||
->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{photo: string, latitude: float, longitude: float, location_tag: string} $validated
|
||||
*/
|
||||
|
||||
31
app/Services/System/Setting/HrSettingService.php
Normal file
31
app/Services/System/Setting/HrSettingService.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\System\Setting;
|
||||
|
||||
use App\Settings\HrSettings;
|
||||
|
||||
class HrSettingService
|
||||
{
|
||||
public function hrData(): array
|
||||
{
|
||||
$settings = app(HrSettings::class);
|
||||
|
||||
return [
|
||||
'scheduled_check_in_time' => $settings->scheduled_check_in_time,
|
||||
'scheduled_check_out_time' => $settings->scheduled_check_out_time,
|
||||
'late_penalty_amount' => $settings->late_penalty_amount,
|
||||
'absent_penalty_amount' => $settings->absent_penalty_amount,
|
||||
];
|
||||
}
|
||||
|
||||
public function updateHr(array $validated): void
|
||||
{
|
||||
$settings = app(HrSettings::class);
|
||||
|
||||
$settings->scheduled_check_in_time = $validated['scheduled_check_in_time'];
|
||||
$settings->scheduled_check_out_time = $validated['scheduled_check_out_time'];
|
||||
$settings->late_penalty_amount = (int) $validated['late_penalty_amount'];
|
||||
$settings->absent_penalty_amount = (int) $validated['absent_penalty_amount'];
|
||||
$settings->save();
|
||||
}
|
||||
}
|
||||
21
app/Settings/HrSettings.php
Normal file
21
app/Settings/HrSettings.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Settings;
|
||||
|
||||
use Spatie\LaravelSettings\Settings;
|
||||
|
||||
class HrSettings extends Settings
|
||||
{
|
||||
public string $scheduled_check_in_time;
|
||||
|
||||
public string $scheduled_check_out_time;
|
||||
|
||||
public int $late_penalty_amount;
|
||||
|
||||
public int $absent_penalty_amount;
|
||||
|
||||
public static function group(): string
|
||||
{
|
||||
return 'hr';
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('payroll_adjustments', function (Blueprint $table): void {
|
||||
$table->foreignId('attendance_id')->nullable()->after('payroll_id')->constrained()->nullOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('payroll_adjustments', function (Blueprint $table): void {
|
||||
$table->dropForeign(['attendance_id']);
|
||||
$table->dropColumn('attendance_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
16
database/settings/2026_06_18_100000_create_hr_settings.php
Normal file
16
database/settings/2026_06_18_100000_create_hr_settings.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
use Spatie\LaravelSettings\Migrations\SettingsBlueprint;
|
||||
use Spatie\LaravelSettings\Migrations\SettingsMigration;
|
||||
|
||||
return new class extends SettingsMigration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$this->migrator->inGroup('hr', function (SettingsBlueprint $blueprint): void {
|
||||
$blueprint->add('scheduled_check_in_time', '08:00');
|
||||
$blueprint->add('late_penalty_amount', 0);
|
||||
$blueprint->add('absent_penalty_amount', 0);
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
use Spatie\LaravelSettings\Migrations\SettingsBlueprint;
|
||||
use Spatie\LaravelSettings\Migrations\SettingsMigration;
|
||||
|
||||
return new class extends SettingsMigration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$this->migrator->inGroup('hr', function (SettingsBlueprint $blueprint): void {
|
||||
$blueprint->add('scheduled_check_in_time', '08:00');
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
use Spatie\LaravelSettings\Migrations\SettingsBlueprint;
|
||||
use Spatie\LaravelSettings\Migrations\SettingsMigration;
|
||||
|
||||
return new class extends SettingsMigration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$this->migrator->inGroup('hr', function (SettingsBlueprint $blueprint): void {
|
||||
$blueprint->add('scheduled_check_out_time', '17:00');
|
||||
});
|
||||
}
|
||||
};
|
||||
88
resources/js/components/admin/setting/HrSection.vue
Normal file
88
resources/js/components/admin/setting/HrSection.vue
Normal file
@ -0,0 +1,88 @@
|
||||
<script setup lang="ts">
|
||||
import { useForm } from '@inertiajs/vue3';
|
||||
import { Save } from '@lucide/vue';
|
||||
import { toast } from 'vue-sonner';
|
||||
import { RupiahInput } from '@/components/form/rupiah-input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Field, FieldError, FieldGroup, FieldLabel, FieldSet } from '@/components/ui/field';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { formErrors } from '@/lib/form';
|
||||
import type { HrSettingsData } from '@/types/setting';
|
||||
|
||||
const props = defineProps<{
|
||||
data: HrSettingsData;
|
||||
}>();
|
||||
|
||||
const form = useForm({
|
||||
scheduled_check_in_time: props.data.scheduled_check_in_time,
|
||||
scheduled_check_out_time: props.data.scheduled_check_out_time,
|
||||
late_penalty_amount: String(props.data.late_penalty_amount),
|
||||
absent_penalty_amount: String(props.data.absent_penalty_amount),
|
||||
});
|
||||
|
||||
function submit() {
|
||||
form.put('/admin/system/setting/hr', {
|
||||
preserveScroll: true,
|
||||
onError: () => {
|
||||
toast.error('Gagal menyimpan pengaturan HR.');
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form @submit.prevent="submit">
|
||||
<Card>
|
||||
<CardContent>
|
||||
<FieldSet>
|
||||
<FieldGroup class="grid gap-4 sm:grid-cols-2">
|
||||
<Field>
|
||||
<FieldLabel for="scheduled_check_in_time" required>Jam Masuk Kerja</FieldLabel>
|
||||
<Input id="scheduled_check_in_time" v-model="form.scheduled_check_in_time" type="time" />
|
||||
<p class="text-xs text-muted-foreground mt-1">
|
||||
Jam masuk yang dijadwalkan. Keterlambatan dihitung dari jam ini. Notifikasi pengingat
|
||||
dikirim 30 menit sebelum jam ini.
|
||||
</p>
|
||||
<FieldError :errors="formErrors(form, 'scheduled_check_in_time')" />
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel for="scheduled_check_out_time" required>Jam Pulang Kerja</FieldLabel>
|
||||
<Input id="scheduled_check_out_time" v-model="form.scheduled_check_out_time" type="time" />
|
||||
<p class="text-xs text-muted-foreground mt-1">
|
||||
Jam pulang yang dijadwalkan. Digunakan sebagai referensi waktu kerja.
|
||||
</p>
|
||||
<FieldError :errors="formErrors(form, 'scheduled_check_out_time')" />
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel for="late_penalty_amount">Denda Keterlambatan</FieldLabel>
|
||||
<RupiahInput id="late_penalty_amount" v-model="form.late_penalty_amount" placeholder="0" />
|
||||
<p class="text-xs text-muted-foreground mt-1">
|
||||
Potongan gaji otomatis jika pegawai terlambat melakukan presensi masuk.
|
||||
</p>
|
||||
<FieldError :errors="formErrors(form, 'late_penalty_amount')" />
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel for="absent_penalty_amount">Denda Bolos</FieldLabel>
|
||||
<RupiahInput id="absent_penalty_amount" v-model="form.absent_penalty_amount" placeholder="0" />
|
||||
<p class="text-xs text-muted-foreground mt-1">
|
||||
Potongan gaji otomatis jika pegawai tidak melakukan presensi masuk dan pulang (bolos).
|
||||
</p>
|
||||
<FieldError :errors="formErrors(form, 'absent_penalty_amount')" />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div class="mt-6 flex justify-end">
|
||||
<Button type="submit" :disabled="form.processing">
|
||||
<Save class="size-4" />
|
||||
{{ form.processing ? 'Menyimpan...' : 'Simpan' }}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { Settings2, Share2, ShoppingBag } from '@lucide/vue';
|
||||
import { Settings2, Share2, ShoppingBag, Users } from '@lucide/vue';
|
||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { SettingSection } from '@/types/setting';
|
||||
@ -14,6 +14,7 @@ const navItems: Array<{
|
||||
{ key: 'system', label: 'Sistem', icon: Settings2 },
|
||||
{ key: 'social', label: 'Media Sosial', icon: Share2 },
|
||||
{ key: 'marketplace', label: 'Marketplace', icon: ShoppingBag },
|
||||
{ key: 'hr', label: 'HR / Pegawai', icon: Users },
|
||||
];
|
||||
</script>
|
||||
|
||||
|
||||
@ -13,6 +13,7 @@ import type { AttendanceListItem, CalendarRange, TodayAttendance } from '@/types
|
||||
const props = defineProps<{
|
||||
attendances: AttendanceListItem[];
|
||||
todayAttendance: TodayAttendance;
|
||||
isOnLeave: boolean;
|
||||
canCheckIn: boolean;
|
||||
canManageAll: boolean;
|
||||
calendarRange: CalendarRange;
|
||||
@ -21,14 +22,19 @@ const props = defineProps<{
|
||||
const webcamModalOpen = ref(false);
|
||||
const webcamMode = ref<'check-in' | 'check-out'>('check-in');
|
||||
|
||||
const canCheckInToday = computed(() => props.canCheckIn && props.todayAttendance === null);
|
||||
const canCheckInToday = computed(() => props.canCheckIn && props.todayAttendance === null && !props.isOnLeave);
|
||||
const canCheckOutToday = computed(() => (
|
||||
props.canCheckIn
|
||||
&& !props.isOnLeave
|
||||
&& props.todayAttendance !== null
|
||||
&& props.todayAttendance.check_out_at === null
|
||||
));
|
||||
|
||||
const todayStatusLabel = computed(() => {
|
||||
if (props.isOnLeave) {
|
||||
return 'Sedang cuti';
|
||||
}
|
||||
|
||||
if (!props.todayAttendance) {
|
||||
return 'Belum presensi';
|
||||
}
|
||||
@ -41,6 +47,10 @@ const todayStatusLabel = computed(() => {
|
||||
});
|
||||
|
||||
const todayStatusVariant = computed(() => {
|
||||
if (props.isOnLeave) {
|
||||
return 'secondary' as const;
|
||||
}
|
||||
|
||||
if (!props.todayAttendance) {
|
||||
return 'secondary' as const;
|
||||
}
|
||||
@ -92,6 +102,10 @@ function openCheckOutModal() {
|
||||
{{ todayStatusLabel }}
|
||||
</Badge>
|
||||
|
||||
<span v-if="isOnLeave" class="text-muted-foreground text-sm">
|
||||
Anda sedang dalam masa cuti. Presensi tidak diperlukan.
|
||||
</span>
|
||||
|
||||
<span v-if="todayAttendance?.check_in_at_formatted" class="text-muted-foreground text-sm">
|
||||
Masuk: {{ todayAttendance.check_in_at_formatted }}
|
||||
</span>
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { Head } from '@inertiajs/vue3';
|
||||
import { ref } from 'vue';
|
||||
import HrSection from '@/components/admin/setting/HrSection.vue';
|
||||
import MarketplaceSection from '@/components/admin/setting/MarketplaceSection.vue';
|
||||
import SocialMediaSection from '@/components/admin/setting/SocialMediaSection.vue';
|
||||
import SystemSection from '@/components/admin/setting/SystemSection.vue';
|
||||
import SettingLayout from '@/layouts/SettingLayout.vue';
|
||||
import type {
|
||||
HrSettingsData,
|
||||
MarketplaceSettingsData,
|
||||
SettingSection,
|
||||
SocialMediaSettingsData,
|
||||
@ -16,6 +18,7 @@ defineProps<{
|
||||
system: SystemSettingsData;
|
||||
socialMedia: SocialMediaSettingsData;
|
||||
marketplace: MarketplaceSettingsData;
|
||||
hr: HrSettingsData;
|
||||
}>();
|
||||
|
||||
const activeSection = ref<SettingSection>('system');
|
||||
@ -29,5 +32,6 @@ const activeSection = ref<SettingSection>('system');
|
||||
<SystemSection v-if="activeSection === 'system'" :data="system" />
|
||||
<SocialMediaSection v-else-if="activeSection === 'social'" :data="socialMedia" />
|
||||
<MarketplaceSection v-else-if="activeSection === 'marketplace'" :data="marketplace" />
|
||||
<HrSection v-else-if="activeSection === 'hr'" :data="hr" />
|
||||
</SettingLayout>
|
||||
</template>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
export type SettingSection = 'system' | 'social' | 'marketplace';
|
||||
export type SettingSection = 'system' | 'social' | 'marketplace' | 'hr';
|
||||
|
||||
export type MarketplacePlatform = 'tiktok_shop' | 'shopee';
|
||||
|
||||
@ -45,3 +45,10 @@ export type MarketplaceSettingsData = {
|
||||
shopee_pre_order: MarketplaceFeeRule;
|
||||
shopee_live_extra: MarketplaceFeeRule;
|
||||
};
|
||||
|
||||
export type HrSettingsData = {
|
||||
scheduled_check_in_time: string;
|
||||
scheduled_check_out_time: string;
|
||||
late_penalty_amount: number;
|
||||
absent_penalty_amount: number;
|
||||
};
|
||||
|
||||
@ -1,8 +1,15 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Inspiring;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Schedule;
|
||||
|
||||
Artisan::command('inspire', function () {
|
||||
$this->comment(Inspiring::quote());
|
||||
})->purpose('Display an inspiring quote');
|
||||
Schedule::command('attendance:send-reminder')
|
||||
->daily()
|
||||
->at('07:30')
|
||||
->withoutOverlapping()
|
||||
->appendOutputTo(storage_path('logs/attendance-reminder.log'));
|
||||
|
||||
Schedule::command('attendance:apply-penalties')
|
||||
->daily()
|
||||
->at('01:00')
|
||||
->withoutOverlapping()
|
||||
->appendOutputTo(storage_path('logs/attendance-penalties.log'));
|
||||
|
||||
@ -321,6 +321,10 @@
|
||||
Route::put('marketplace', [SettingController::class, 'updateMarketplace'])
|
||||
->middleware('permission:'.Permission::SETTINGS_UPDATE->value)
|
||||
->name('marketplace.update');
|
||||
|
||||
Route::put('hr', [SettingController::class, 'updateHr'])
|
||||
->middleware('permission:'.Permission::SETTINGS_UPDATE->value)
|
||||
->name('hr.update');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user