feat: implement attendance tracking system with admin and employee dashboards

This commit is contained in:
IlhamIslamy 2026-04-27 22:45:22 +07:00
parent 859dbe13a1
commit a3ad24e029
14 changed files with 925 additions and 26 deletions

View File

@ -0,0 +1,45 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Attendance;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Carbon\Carbon;
class AttendanceController extends Controller
{
public function index()
{
$attendances = Attendance::with('employee')->orderBy('date', 'desc')->get();
$attendances = $attendances->map(function ($attendance) {
$totalHours = null;
// Menghitung selisih jam kerja dalam format H:i
if ($attendance->check_in && $attendance->check_out) {
$checkIn = Carbon::parse($attendance->check_in);
$checkOut = Carbon::parse($attendance->check_out);
$diff = $checkIn->diff($checkOut);
$totalHours = sprintf('%02d:%02d', $diff->h, $diff->i);
}
return [
'id' => $attendance->id,
'employee_name' => $attendance->employee->name ?? '-',
'employee_nik' => $attendance->employee->nip ?? '-',
'date' => $attendance->date,
'check_in' => $attendance->check_in,
'check_out' => $attendance->check_out,
'status' => $attendance->status,
'notes' => $attendance->notes,
'total_hours' => $totalHours,
];
});
return Inertia::render('admin/attendances/index', [
'attendances' => $attendances
]);
}
}

View File

@ -0,0 +1,153 @@
<?php
namespace App\Http\Controllers\Employee;
use App\Http\Controllers\Controller;
use App\Models\Attendance;
use Illuminate\Http\Request;
use Carbon\Carbon;
use Inertia\Inertia;
class EmployeeAttendanceController extends Controller
{
private function checkEmployee(Request $request)
{
return $request->user()->employee;
}
public function index(Request $request)
{
$employee = $this->checkEmployee($request);
if (!$employee) {
return redirect()->route('employee.index')->with('error', 'Profil karyawan tidak ditemukan.');
}
$employeeId = $employee->id;
$attendances = Attendance::where('employee_id', $employeeId)->orderBy('date', 'desc')->get();
$today = Carbon::today()->toDateString();
$todayAttendance = Attendance::where('employee_id', $employeeId)->where('date', $today)->first();
return Inertia::render('employee/attendances/index', [
'attendances' => $attendances,
'todayAttendance' => $todayAttendance,
]);
}
public function clockIn(Request $request)
{
$employee = $this->checkEmployee($request);
if (!$employee) {
return redirect()->route('employee.index')->with('error', 'Profil karyawan tidak ditemukan.');
}
$request->validate([
'latitude_in' => 'required|numeric',
'longitude_in' => 'required|numeric',
]);
$employeeId = $employee->id;
$today = Carbon::today()->toDateString();
$existingAttendance = Attendance::where('employee_id', $employeeId)
->where('date', $today)
->first();
if ($existingAttendance) {
return back()->with('error', 'Anda sudah melakukan absen masuk hari ini.');
}
Attendance::create([
'employee_id' => $employeeId,
'date' => $today,
'check_in' => Carbon::now()->toTimeString(),
'latitude_in' => $request->latitude_in,
'longitude_in' => $request->longitude_in,
'status' => 'present',
]);
return back()->with('success', 'Berhasil Absen Masuk.');
}
public function clockOut(Request $request)
{
$employee = $this->checkEmployee($request);
if (!$employee) {
return redirect()->route('employee.index')->with('error', 'Profil karyawan tidak ditemukan.');
}
$request->validate([
'latitude_out' => 'required|numeric',
'longitude_out' => 'required|numeric',
]);
$employeeId = $employee->id;
$today = Carbon::today()->toDateString();
$attendance = Attendance::where('employee_id', $employeeId)
->where('date', $today)
->first();
if (!$attendance || $attendance->check_out) {
return back()->with('error', 'Anda belum absen masuk atau sudah absen pulang hari ini.');
}
$attendance->update([
'check_out' => Carbon::now()->toTimeString(),
'latitude_out' => $request->latitude_out,
'longitude_out' => $request->longitude_out,
]);
return back()->with('success', 'Berhasil Absen Pulang.');
}
public function submitLeave(Request $request)
{
$employee = $this->checkEmployee($request);
if (!$employee) {
return redirect()->route('employee.index')->with('error', 'Profil karyawan tidak ditemukan.');
}
$request->validate([
'date' => 'required|date',
'notes' => 'required|string',
]);
Attendance::create([
'employee_id' => $employee->id,
'date' => $request->date,
'status' => 'leave',
'notes' => $request->notes,
]);
return back()->with('success', 'Izin berhasil diajukan.');
}
public function submitDispensation(Request $request)
{
$employee = $this->checkEmployee($request);
if (!$employee) {
return redirect()->route('employee.index')->with('error', 'Profil karyawan tidak ditemukan.');
}
$request->validate([
'latitude_in' => 'required|numeric',
'longitude_in' => 'required|numeric',
'notes' => 'required|string',
]);
$now = Carbon::now();
Attendance::create([
'employee_id' => $employee->id,
'date' => $now->toDateString(),
'check_in' => $now->toTimeString(),
'latitude_in' => $request->latitude_in,
'longitude_in' => $request->longitude_in,
'status' => 'dispensation',
'notes' => $request->notes,
]);
return back()->with('success', 'Dispensasi berhasil diajukan.');
}
}

View File

@ -45,6 +45,10 @@ public function share(Request $request): array
'role' => $request->user()->role,
] : null,
],
'flash' => [
'success' => fn () => $request->session()->get('success'),
'error' => fn () => $request->session()->get('error'),
],
];
}
}

30
app/Models/Attendance.php Normal file
View File

@ -0,0 +1,30 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Attendance extends Model
{
use HasFactory;
protected $fillable = [
'employee_id',
'date',
'check_in',
'check_out',
'latitude_in',
'longitude_in',
'latitude_out',
'longitude_out',
'status',
'notes',
];
public function employee(): BelongsTo
{
return $this->belongsTo(Employee::class);
}
}

View File

@ -0,0 +1,31 @@
<?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::create('attendances', function (Blueprint $table) {
$table->id();
$table->foreignId('employee_id')->constrained()->onDelete('cascade');
$table->date('date');
$table->time('check_in')->nullable();
$table->time('check_out')->nullable();
$table->decimal('latitude_in', 10, 8)->nullable();
$table->decimal('longitude_in', 11, 8)->nullable();
$table->decimal('latitude_out', 10, 8)->nullable();
$table->decimal('longitude_out', 11, 8)->nullable();
$table->enum('status', ['present', 'leave', 'dispensation'])->default('present');
$table->text('notes')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('attendances');
}
};

View File

@ -13,49 +13,49 @@ class EmployeeSeeder extends Seeder
{
public function run(): void
{
$depts = Department::pluck('id', 'name');
$depts = Department::pluck('id', 'name');
$positions = Position::pluck('id', 'name');
$employees = [
[
'user' => ['name' => 'Jono Joni', 'email' => 'jono12@gmail.com', 'password' => Hash::make('password'), 'role' => 'employee'],
'employee' => ['nip' => '2023001', 'name' => 'Jono Joni', 'gender' => 'L', 'place_of_birth' => 'Jakarta', 'birth_date' => '1992-05-14', 'address' => 'Jl. Merdeka No.12, Jakarta Pusat', 'phone_number' => '081234567890', 'department_id' => $depts['Information Technology'], 'position_id' => $positions['Senior Developer'], 'status' => 'PKWTT', 'join_date' => '2023-01-15'],
'user' => ['name' => 'Jono Joni', 'email' => 'jono12@gmail.com', 'password' => Hash::make('password'), 'role' => 'employee'],
'employee' => ['nip' => '2023001', 'name' => 'Jono Joni', 'gender' => 'L', 'place_of_birth' => 'Jakarta', 'birth_date' => '1992-05-14', 'address' => 'Jl. Merdeka No.12, Jakarta Pusat', 'phone_number' => '081234567890', 'department_id' => $depts['Information Technology'], 'position_id' => $positions['Senior Developer'], 'status' => 'PKWTT', 'join_date' => '2023-01-15'],
],
[
'user' => ['name' => 'Budi Santoso', 'email' => 'budi.santoso12@hris.com', 'password' => Hash::make('password'), 'role' => 'employee'],
'employee' => ['nip' => '2023002', 'name' => 'Budi Santoso', 'gender' => 'L', 'place_of_birth' => 'Bandung', 'birth_date' => '1995-08-22', 'address' => 'Jl. Sukajadi No.45, Bandung', 'phone_number' => '082345678901', 'department_id' => $depts['Information Technology'], 'position_id' => $positions['Junior Developer'], 'status' => 'PKWT', 'join_date' => '2023-03-01'],
'user' => ['name' => 'Budi Santoso', 'email' => 'budi.santoso12@hris.com', 'password' => Hash::make('password'), 'role' => 'employee'],
'employee' => ['nip' => '2023002', 'name' => 'Budi Santoso', 'gender' => 'L', 'place_of_birth' => 'Bandung', 'birth_date' => '1995-08-22', 'address' => 'Jl. Sukajadi No.45, Bandung', 'phone_number' => '082345678901', 'department_id' => $depts['Information Technology'], 'position_id' => $positions['Junior Developer'], 'status' => 'PKWT', 'join_date' => '2023-03-01'],
],
[
'user' => ['name' => 'Rizky Firmansyah', 'email' => 'rizky.firmansyah12@hris.com', 'password' => Hash::make('password'), 'role' => 'employee'],
'employee' => ['nip' => '2024001', 'name' => 'Rizky Firmansyah', 'gender' => 'L', 'place_of_birth' => 'Surabaya', 'birth_date' => '1998-11-30', 'address' => 'Jl. Pemuda No.7, Surabaya', 'phone_number' => '083456789012', 'department_id' => $depts['Information Technology'], 'position_id' => $positions['Junior Developer'], 'status' => 'Magang','join_date' => '2024-02-01'],
'user' => ['name' => 'Rizky Firmansyah', 'email' => 'rizky.firmansyah12@hris.com', 'password' => Hash::make('password'), 'role' => 'employee'],
'employee' => ['nip' => '2024001', 'name' => 'Rizky Firmansyah', 'gender' => 'L', 'place_of_birth' => 'Surabaya', 'birth_date' => '1998-11-30', 'address' => 'Jl. Pemuda No.7, Surabaya', 'phone_number' => '083456789012', 'department_id' => $depts['Information Technology'], 'position_id' => $positions['Junior Developer'], 'status' => 'Magang', 'join_date' => '2024-02-01'],
],
[
'user' => ['name' => 'Dewi Rahayu', 'email' => 'dewi.rahayu12@hris.com', 'password' => Hash::make('password'), 'role' => 'employee'],
'employee' => ['nip' => '2022001', 'name' => 'Dewi Rahayu', 'gender' => 'P', 'place_of_birth' => 'Yogyakarta', 'birth_date' => '1990-03-10', 'address' => 'Jl. Malioboro No.88, Yogyakarta', 'phone_number' => '084567890123', 'department_id' => $depts['Human Resources'], 'position_id' => $positions['Manager'], 'status' => 'PKWTT', 'join_date' => '2022-06-01'],
'user' => ['name' => 'Dewi Rahayu', 'email' => 'dewi.rahayu12@hris.com', 'password' => Hash::make('password'), 'role' => 'employee'],
'employee' => ['nip' => '2022001', 'name' => 'Dewi Rahayu', 'gender' => 'P', 'place_of_birth' => 'Yogyakarta', 'birth_date' => '1990-03-10', 'address' => 'Jl. Malioboro No.88, Yogyakarta', 'phone_number' => '084567890123', 'department_id' => $depts['Human Resources'], 'position_id' => $positions['Manager'], 'status' => 'PKWTT', 'join_date' => '2022-06-01'],
],
[
'user' => ['name' => 'Anisa Putri', 'email' => 'anisa.putri12@hris.com', 'password' => Hash::make('password'), 'role' => 'employee'],
'employee' => ['nip' => '2023003', 'name' => 'Anisa Putri', 'gender' => 'P', 'place_of_birth' => 'Semarang', 'birth_date' => '1997-07-19', 'address' => 'Jl. Pahlawan No.3, Semarang', 'phone_number' => '085678901234', 'department_id' => $depts['Human Resources'], 'position_id' => $positions['HR Specialist'], 'status' => 'PKWTT', 'join_date' => '2023-07-01'],
'user' => ['name' => 'Anisa Putri', 'email' => 'anisa.putri12@hris.com', 'password' => Hash::make('password'), 'role' => 'employee'],
'employee' => ['nip' => '2023003', 'name' => 'Anisa Putri', 'gender' => 'P', 'place_of_birth' => 'Semarang', 'birth_date' => '1997-07-19', 'address' => 'Jl. Pahlawan No.3, Semarang', 'phone_number' => '085678901234', 'department_id' => $depts['Human Resources'], 'position_id' => $positions['HR Specialist'], 'status' => 'PKWTT', 'join_date' => '2023-07-01'],
],
[
'user' => ['name' => 'Hendra Kurniawan', 'email' => 'hendra.kurniawan12@hris.com', 'password' => Hash::make('password'), 'role' => 'employee'],
'employee' => ['nip' => '2021001', 'name' => 'Hendra Kurniawan', 'gender' => 'L', 'place_of_birth' => 'Medan', 'birth_date' => '1988-12-05', 'address' => 'Jl. Sudirman No.22, Medan', 'phone_number' => '086789012345', 'department_id' => $depts['Finance'], 'position_id' => $positions['Finance Analyst'], 'status' => 'PKWTT', 'join_date' => '2021-04-01'],
'user' => ['name' => 'Hendra Kurniawan', 'email' => 'hendra.kurniawan12@hris.com', 'password' => Hash::make('password'), 'role' => 'employee'],
'employee' => ['nip' => '2021001', 'name' => 'Hendra Kurniawan', 'gender' => 'L', 'place_of_birth' => 'Medan', 'birth_date' => '1988-12-05', 'address' => 'Jl. Sudirman No.22, Medan', 'phone_number' => '086789012345', 'department_id' => $depts['Finance'], 'position_id' => $positions['Finance Analyst'], 'status' => 'PKWTT', 'join_date' => '2021-04-01'],
],
[
'user' => ['name' => 'Sari Wulandari', 'email' => 'sari.wulandari12@hris.com', 'password' => Hash::make('password'), 'role' => 'employee'],
'employee' => ['nip' => '2023004', 'name' => 'Sari Wulandari', 'gender' => 'P', 'place_of_birth' => 'Solo', 'birth_date' => '1996-02-28', 'address' => 'Jl. Brigjen Katamso No.5, Solo', 'phone_number' => '087890123456', 'department_id' => $depts['Finance'], 'position_id' => $positions['Staff Admin'], 'status' => 'PKWT', 'join_date' => '2023-09-01'],
'user' => ['name' => 'Sari Wulandari', 'email' => 'sari.wulandari12@hris.com', 'password' => Hash::make('password'), 'role' => 'employee'],
'employee' => ['nip' => '2023004', 'name' => 'Sari Wulandari', 'gender' => 'P', 'place_of_birth' => 'Solo', 'birth_date' => '1996-02-28', 'address' => 'Jl. Brigjen Katamso No.5, Solo', 'phone_number' => '087890123456', 'department_id' => $depts['Finance'], 'position_id' => $positions['Staff Admin'], 'status' => 'PKWT', 'join_date' => '2023-09-01'],
],
[
'user' => ['name' => 'Fajar Nugroho', 'email' => 'fajar.nugroho12@hris.com', 'password' => Hash::make('password'), 'role' => 'employee'],
'employee' => ['nip' => '2022002', 'name' => 'Fajar Nugroho', 'gender' => 'L', 'place_of_birth' => 'Makassar', 'birth_date' => '1993-09-15', 'address' => 'Jl. Sultan Hasanuddin No.10, Makassar', 'phone_number' => '088901234567', 'department_id' => $depts['Marketing'], 'position_id' => $positions['Marketing Staff'], 'status' => 'PKWTT', 'join_date' => '2022-11-01'],
'user' => ['name' => 'Fajar Nugroho', 'email' => 'fajar.nugroho12@hris.com', 'password' => Hash::make('password'), 'role' => 'employee'],
'employee' => ['nip' => '2022002', 'name' => 'Fajar Nugroho', 'gender' => 'L', 'place_of_birth' => 'Makassar', 'birth_date' => '1993-09-15', 'address' => 'Jl. Sultan Hasanuddin No.10, Makassar', 'phone_number' => '088901234567', 'department_id' => $depts['Marketing'], 'position_id' => $positions['Marketing Staff'], 'status' => 'PKWTT', 'join_date' => '2022-11-01'],
],
[
'user' => ['name' => 'Linda Permatasari','email' => 'linda.permata12@hris.com', 'password' => Hash::make('password'), 'role' => 'employee'],
'employee' => ['nip' => '2024002', 'name' => 'Linda Permatasari','gender' => 'P', 'place_of_birth' => 'Palembang', 'birth_date' => '1999-04-20', 'address' => 'Jl. Demang Lebar Daun No.8, Palembang', 'phone_number' => '089012345678', 'department_id' => $depts['Operations'], 'position_id' => $positions['Operations Staff'], 'status' => 'PKWT', 'join_date' => '2024-01-10'],
'user' => ['name' => 'Linda Permatasari', 'email' => 'linda.permata12@hris.com', 'password' => Hash::make('password'), 'role' => 'employee'],
'employee' => ['nip' => '2024002', 'name' => 'Linda Permatasari', 'gender' => 'P', 'place_of_birth' => 'Palembang', 'birth_date' => '1999-04-20', 'address' => 'Jl. Demang Lebar Daun No.8, Palembang', 'phone_number' => '089012345678', 'department_id' => $depts['Operations'], 'position_id' => $positions['Operations Staff'], 'status' => 'PKWT', 'join_date' => '2024-01-10'],
],
[
'user' => ['name' => 'Agus Prasetyo', 'email' => 'agus.prasetyo12@hris.com', 'password' => Hash::make('password'), 'role' => 'employee'],
'employee' => ['nip' => '2020001', 'name' => 'Agus Prasetyo', 'gender' => 'L', 'place_of_birth' => 'Malang', 'birth_date' => '1985-06-10', 'address' => 'Jl. Ijen No.33, Malang', 'phone_number' => '081112233445', 'department_id' => $depts['Operations'], 'position_id' => $positions['Manager'], 'status' => 'PKWTT', 'join_date' => '2020-08-01'],
'user' => ['name' => 'Agus Prasetyo', 'email' => 'agus.prasetyo12@hris.com', 'password' => Hash::make('password'), 'role' => 'employee'],
'employee' => ['nip' => '2020001', 'name' => 'Agus Prasetyo', 'gender' => 'L', 'place_of_birth' => 'Malang', 'birth_date' => '1985-06-10', 'address' => 'Jl. Ijen No.33, Malang', 'phone_number' => '081112233445', 'department_id' => $depts['Operations'], 'position_id' => $positions['Manager'], 'status' => 'PKWTT', 'join_date' => '2020-08-01'],
],
];

23
package-lock.json generated
View File

@ -94,6 +94,7 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
@ -5537,6 +5538,7 @@
"integrity": "sha512-ebO/Yl+EAvVe8DnMfi+iaAyIqYdK0q/q0y0rw82INWEKJOBe6b/P3YWE8NW7oOlF/nXFNrHwhARrN/hdgDkraA==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~6.21.0"
}
@ -5546,6 +5548,7 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.10.tgz",
"integrity": "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==",
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@ -5555,6 +5558,7 @@
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "^19.2.0"
}
@ -5610,6 +5614,7 @@
"integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.54.0",
"@typescript-eslint/types": "8.54.0",
@ -6129,6 +6134,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@ -6465,6 +6471,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@ -7277,6 +7284,7 @@
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@ -7463,6 +7471,7 @@
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9",
@ -9574,6 +9583,7 @@
"integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@ -9925,6 +9935,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@ -9934,6 +9945,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@ -9945,13 +9957,15 @@
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/react-redux": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/use-sync-external-store": "^0.0.6",
"use-sync-external-store": "^1.4.0"
@ -10082,7 +10096,8 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/redux-thunk": {
"version": "3.1.0",
@ -10922,6 +10937,7 @@
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@ -10987,6 +11003,7 @@
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"dependencies": {
"napi-postinstall": "^0.3.0"
},
@ -11146,6 +11163,7 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.27.0",
"fdir": "^6.5.0",
@ -11430,6 +11448,7 @@
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"dev": true,
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}

View File

@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Link, usePage } from '@inertiajs/react';
import { LayoutGrid, Users, Briefcase, Building2, SquareUser, Wallet } from 'lucide-react';
import { LayoutGrid, Users, Briefcase, Building2, SquareUser, Wallet, CalendarClock } from 'lucide-react';
import { NavMain } from '@/components/nav-main';
import { NavUser } from '@/components/nav-user';
import {
@ -47,12 +47,24 @@ export function AppSidebar() {
href: '/admin/payrolls',
icon: Wallet,
},
{
title: 'Manajemen Absensi',
href: '/admin/attendances',
icon: CalendarClock,
},
{
title: 'Manajemen Pengguna',
href: '/admin/users',
icon: SquareUser,
},
] : []),
...(userRole === 'employee' ? [
{
title: 'Absensi',
href: '/employee/attendances',
icon: CalendarClock,
},
] : []),
];
return (

View File

@ -0,0 +1,89 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Tabs as TabsPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
function Tabs({
className,
orientation = "horizontal",
...props
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
return (
<TabsPrimitive.Root
data-slot="tabs"
data-orientation={orientation}
orientation={orientation}
className={cn(
"group/tabs flex gap-2 data-[orientation=horizontal]:flex-col",
className
)}
{...props}
/>
)
}
const tabsListVariants = cva(
"group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-[orientation=horizontal]/tabs:h-9 group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col data-[variant=line]:rounded-none",
{
variants: {
variant: {
default: "bg-muted",
line: "gap-1 bg-transparent",
},
},
defaultVariants: {
variant: "default",
},
}
)
function TabsList({
className,
variant = "default",
...props
}: React.ComponentProps<typeof TabsPrimitive.List> &
VariantProps<typeof tabsListVariants>) {
return (
<TabsPrimitive.List
data-slot="tabs-list"
data-variant={variant}
className={cn(tabsListVariants({ variant }), className)}
{...props}
/>
)
}
function TabsTrigger({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
return (
<TabsPrimitive.Trigger
data-slot="tabs-trigger"
className={cn(
"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none dark:text-muted-foreground dark:hover:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent",
"data-[state=active]:bg-background data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 dark:data-[state=active]:text-foreground",
"after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100",
className
)}
{...props}
/>
)
}
function TabsContent({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
return (
<TabsPrimitive.Content
data-slot="tabs-content"
className={cn("flex-1 outline-none", className)}
{...props}
/>
)
}
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }

View File

@ -0,0 +1,18 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
<textarea
data-slot="textarea"
className={cn(
"flex field-sizing-content min-h-16 w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:aria-invalid:ring-destructive/40",
className
)}
{...props}
/>
)
}
export { Textarea }

View File

@ -0,0 +1,101 @@
import { Head, usePage } from '@inertiajs/react';
import React from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Separator } from '@/components/ui/separator';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import AppLayout from '@/layouts/app-layout';
interface Attendance {
id: number;
employee_name: string;
employee_nik: string;
date: string;
check_in: string | null;
check_out: string | null;
status: 'present' | 'leave' | 'dispensation';
notes: string | null;
total_hours: string | null;
}
interface PageProps {
attendances: Attendance[];
[key: string]: unknown;
}
export default function Index({ attendances }: PageProps) {
const formatStatus = (status: string) => {
switch (status) {
case 'present': return <span className="text-green-600 font-medium">Hadir</span>;
case 'leave': return <span className="text-yellow-600 font-medium">Izin/Cuti</span>;
case 'dispensation': return <span className="text-blue-600 font-medium">Dispensasi</span>;
default: return <span>{status}</span>;
}
};
return (
<AppLayout>
<Head title="Rekap Absensi" />
<div className="p-4 md:p-8 w-full space-y-4">
<div className="space-y-1">
<h2 className="text-2xl font-bold tracking-tight">Rekap Absensi Karyawan</h2>
<p className="text-muted-foreground">Monitoring kehadiran, izin, dan dispensasi karyawan.</p>
</div>
<Card>
<CardHeader className="pb-4">
<CardTitle>Daftar Absensi</CardTitle>
</CardHeader>
<Separator />
<CardContent className="pt-0">
<div className="relative w-full overflow-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>Nama</TableHead>
<TableHead>NIK</TableHead>
<TableHead>Tanggal</TableHead>
<TableHead>Jam Masuk</TableHead>
<TableHead>Jam Keluar</TableHead>
<TableHead>Keterangan</TableHead>
<TableHead className="text-right">Total Jam Kerja</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{attendances.length > 0 ? (
attendances.map((attendance) => (
<TableRow key={attendance.id}>
<TableCell className="font-medium text-gray-900">{attendance.employee_name}</TableCell>
<TableCell className="text-gray-600">{attendance.employee_nik}</TableCell>
<TableCell className="text-gray-600">{attendance.date}</TableCell>
<TableCell>{attendance.check_in || '-'}</TableCell>
<TableCell>{attendance.check_out || '-'}</TableCell>
<TableCell>
<div className="flex flex-col">
{formatStatus(attendance.status)}
{attendance.notes && (
<span className="text-xs text-gray-500 mt-1">{attendance.notes}</span>
)}
</div>
</TableCell>
<TableCell className="text-right font-medium">
{attendance.total_hours ? `${attendance.total_hours} Jam` : '-'}
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={7} className="h-24 text-center text-muted-foreground">
Belum ada data absensi.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
</div>
</AppLayout>
);
}

View File

@ -0,0 +1,378 @@
import { Head, useForm, usePage } from '@inertiajs/react';
import React, { useState, useEffect } from 'react';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import AppLayout from '@/layouts/app-layout';
// Define expected prop types
interface Attendance {
id: number;
date: string;
check_in: string | null;
check_out: string | null;
status: 'present' | 'leave' | 'dispensation';
notes: string | null;
}
interface PageProps {
attendances: Attendance[];
todayAttendance: Attendance | null;
[key: string]: unknown;
}
interface SharedData {
flash: {
success?: string;
error?: string;
};
}
export default function Index({ attendances, todayAttendance }: PageProps) {
const { flash } = usePage<any>().props as SharedData;
const [locationError, setLocationError] = useState<string | null>(null);
const [coordinates, setCoordinates] = useState<{ lat: number; lng: number } | null>(null);
const [isLoadingLocation, setIsLoadingLocation] = useState<boolean>(false);
const {
data: regulerData,
setData: setRegulerData,
post: postReguler,
processing: processingReguler
} = useForm({
latitude_in: '',
longitude_in: '',
latitude_out: '',
longitude_out: '',
});
const {
data: leaveData,
setData: setLeaveData,
post: postLeave,
processing: processingLeave,
reset: resetLeave
} = useForm({
date: '',
notes: '',
});
const {
data: dispenData,
setData: setDispenData,
post: postDispen,
processing: processingDispen,
reset: resetDispen
} = useForm({
latitude_in: '',
longitude_in: '',
notes: '',
});
// Mendapatkan lokasi saat komponen dimuat atau tombol ditekan
const getLocation = (callback?: (lat: number, lng: number) => void) => {
setIsLoadingLocation(true);
setLocationError(null);
if (!navigator.geolocation) {
setLocationError('Geolocation tidak didukung oleh browser ini.');
setIsLoadingLocation(false);
return;
}
navigator.geolocation.getCurrentPosition(
(position) => {
const lat = position.coords.latitude;
const lng = position.coords.longitude;
setCoordinates({ lat, lng });
setRegulerData({
...regulerData,
latitude_in: lat.toString(),
longitude_in: lng.toString(),
latitude_out: lat.toString(),
longitude_out: lng.toString(),
});
setDispenData({
...dispenData,
latitude_in: lat.toString(),
longitude_in: lng.toString(),
});
setIsLoadingLocation(false);
if (callback) callback(lat, lng);
},
(error) => {
let errorMsg = 'Gagal mengambil lokasi.';
if (error.code === 1) errorMsg = 'Izin lokasi ditolak. Harap izinkan akses lokasi.';
setLocationError(errorMsg);
setIsLoadingLocation(false);
},
{ enableHighAccuracy: true, timeout: 10000, maximumAge: 0 }
);
};
useEffect(() => {
getLocation();
}, []);
const handleClockIn = (e: React.FormEvent) => {
e.preventDefault();
if (!coordinates) {
getLocation((lat, lng) => {
postReguler('/employee/attendances/clock-in');
});
} else {
postReguler('/employee/attendances/clock-in');
}
};
const handleClockOut = (e: React.FormEvent) => {
e.preventDefault();
if (!coordinates) {
getLocation((lat, lng) => {
postReguler('/employee/attendances/clock-out');
});
} else {
postReguler('/employee/attendances/clock-out');
}
};
const handleLeaveSubmit = (e: React.FormEvent) => {
e.preventDefault();
postLeave('/employee/attendances/leave', {
onSuccess: () => resetLeave()
});
};
const handleDispenSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!coordinates) {
getLocation((lat, lng) => {
postDispen('/employee/attendances/dispensation', {
onSuccess: () => resetDispen()
});
});
} else {
postDispen('/employee/attendances/dispensation', {
onSuccess: () => resetDispen()
});
}
};
const formatStatus = (status: string) => {
switch (status) {
case 'present': return 'Hadir';
case 'leave': return 'Izin/Cuti';
case 'dispensation': return 'Dispensasi';
default: return status;
}
};
return (
<AppLayout>
<Head title="Absensi Karyawan" />
<div className="p-4 md:p-8 max-w-5xl mx-auto space-y-6">
{/* Header Section */}
<div className="space-y-1">
<h2 className="text-2xl font-bold tracking-tight">Portal Absensi</h2>
<p className="text-muted-foreground">Lakukan absen masuk/pulang, atau ajukan izin dan dispensasi.</p>
</div>
{/* Flash Messages */}
{flash?.success && (
<div className="p-4 bg-green-50 text-green-700 border border-green-200 rounded-md text-sm">
{flash.success}
</div>
)}
{flash?.error && (
<div className="p-4 bg-red-50 text-red-700 border border-red-200 rounded-md text-sm">
{flash.error}
</div>
)}
{locationError && (
<div className="p-4 bg-yellow-50 text-yellow-700 border border-yellow-200 rounded-md text-sm">
{locationError} <Button variant="link" className="p-0 h-auto font-bold text-yellow-800" onClick={() => getLocation()}>Coba Lagi</Button>
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{/* Left Column: Forms */}
<div className="md:col-span-1 space-y-6">
<Tabs defaultValue="reguler" className="w-full">
<TabsList className="w-full grid grid-cols-3">
<TabsTrigger value="reguler">Harian</TabsTrigger>
<TabsTrigger value="izin">Izin</TabsTrigger>
<TabsTrigger value="dispen">Dispen</TabsTrigger>
</TabsList>
{/* Tab Absen Reguler */}
<TabsContent value="reguler">
<Card>
<CardHeader>
<CardTitle>Absen Reguler</CardTitle>
<CardDescription>
{todayAttendance
? 'Anda sudah memiliki catatan absensi hari ini.'
: 'Sistem membutuhkan akses lokasi untuk mencatat absensi.'}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="p-4 bg-zinc-50 border rounded-lg text-center space-y-2">
<p className="text-sm text-muted-foreground">Lokasi Saat Ini:</p>
<p className="font-mono text-xs font-semibold">
{isLoadingLocation ? 'Mengambil lokasi...' : (coordinates ? `${coordinates.lat.toFixed(6)}, ${coordinates.lng.toFixed(6)}` : 'Lokasi tidak tersedia')}
</p>
</div>
<div className="grid grid-cols-2 gap-2">
<Button
onClick={handleClockIn}
disabled={processingReguler || (todayAttendance && todayAttendance.check_in !== null)}
className="w-full bg-blue-600 hover:bg-blue-700"
>
Clock In
</Button>
<Button
onClick={handleClockOut}
disabled={processingReguler || !todayAttendance || (todayAttendance && todayAttendance.check_out !== null)}
variant="outline"
className="w-full"
>
Clock Out
</Button>
</div>
</CardContent>
</Card>
</TabsContent>
{/* Tab Izin */}
<TabsContent value="izin">
<Card>
<CardHeader>
<CardTitle>Form Izin / Cuti</CardTitle>
<CardDescription>Ajukan izin tidak masuk kerja.</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleLeaveSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="date">Tanggal Izin</Label>
<Input
id="date"
type="date"
required
value={leaveData.date}
onChange={e => setLeaveData('date', e.target.value)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="leave_notes">Keterangan (Sakit/Cuti/Dll)</Label>
<Textarea
id="leave_notes"
placeholder="Tulis alasan izin..."
required
value={leaveData.notes}
onChange={e => setLeaveData('notes', e.target.value)}
/>
</div>
<Button type="submit" disabled={processingLeave} className="w-full">
Submit Izin
</Button>
</form>
</CardContent>
</Card>
</TabsContent>
{/* Tab Dispensasi */}
<TabsContent value="dispen">
<Card>
<CardHeader>
<CardTitle>Form Dispensasi</CardTitle>
<CardDescription>Dispensasi tugas luar. Memerlukan lokasi.</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleDispenSubmit} className="space-y-4">
<div className="p-3 bg-zinc-50 border rounded-lg text-center mb-4">
<p className="text-xs text-muted-foreground">Lokasi Tercatat:</p>
<p className="font-mono text-xs font-semibold">
{isLoadingLocation ? 'Mengambil lokasi...' : (coordinates ? `${coordinates.lat.toFixed(6)}, ${coordinates.lng.toFixed(6)}` : 'Lokasi tidak tersedia')}
</p>
</div>
<div className="space-y-2">
<Label htmlFor="dispen_notes">Tujuan / Keterangan</Label>
<Textarea
id="dispen_notes"
placeholder="Contoh: Meeting dengan klien X di lokasi Y"
required
value={dispenData.notes}
onChange={e => setDispenData('notes', e.target.value)}
/>
</div>
<Button type="submit" disabled={processingDispen} className="w-full">
Submit Dispensasi
</Button>
</form>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
{/* Right Column: History */}
<div className="md:col-span-2">
<Card className="h-full">
<CardHeader>
<CardTitle>Riwayat Absensi</CardTitle>
</CardHeader>
<CardContent>
<div className="relative w-full overflow-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>Tanggal</TableHead>
<TableHead>Jam Masuk</TableHead>
<TableHead>Jam Keluar</TableHead>
<TableHead>Status</TableHead>
<TableHead>Keterangan</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{attendances.length > 0 ? (
attendances.map((att) => (
<TableRow key={att.id}>
<TableCell>{att.date}</TableCell>
<TableCell>{att.check_in || '-'}</TableCell>
<TableCell>{att.check_out || '-'}</TableCell>
<TableCell>
<span className="inline-block px-2 py-1 text-xs font-medium bg-gray-100 rounded">
{formatStatus(att.status)}
</span>
</TableCell>
<TableCell className="text-xs text-muted-foreground max-w-[200px] truncate">
{att.notes || '-'}
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={5} className="h-24 text-center text-muted-foreground">
Belum ada riwayat absensi.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
</div>
</div>
</div>
</AppLayout>
);
}

View File

@ -12,7 +12,7 @@ const breadcrumbs: BreadcrumbItem[] = [
];
export default function EmployeeIndex() {
const { auth } = usePage<SharedData>().props;
const { auth, flash } = usePage<any>().props;
const user = auth.user;
return (
@ -30,6 +30,18 @@ export default function EmployeeIndex() {
</p>
</div>
{/* Flash Messages */}
{flash?.error && (
<div className="p-4 bg-red-50 text-red-700 border border-red-200 rounded-md text-sm">
{flash.error}
</div>
)}
{flash?.success && (
<div className="p-4 bg-green-50 text-green-700 border border-green-200 rounded-md text-sm">
{flash.success}
</div>
)}
{/* Kartu Info Akun */}
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
<Card>

View File

@ -23,6 +23,7 @@
Route::resource('positions', PositionController::class);
Route::resource('employees', EmployeeController::class);
Route::resource('payrolls', PayrollController::class)->only(['index', 'create', 'store', 'show']);
Route::get('attendances', [\App\Http\Controllers\Admin\AttendanceController::class, 'index'])->name('attendances.index');
Route::get('users', [UserController::class, 'index'])->name('users.index');
Route::get('users/create', [UserController::class, 'create'])->name('users.create');
@ -33,8 +34,14 @@
// KELOMPOK KARYAWAN
Route::middleware(['auth', 'verified', 'role:employee'])->group(function () {
Route::get('/employee/index', function () {
return Inertia::render('employee/index');
return Inertia\Inertia::render('employee/index');
})->name('employee.index');
Route::get('/employee/attendances', [\App\Http\Controllers\Employee\EmployeeAttendanceController::class, 'index'])->name('employee.attendances.index');
Route::post('/employee/attendances/clock-in', [\App\Http\Controllers\Employee\EmployeeAttendanceController::class, 'clockIn'])->name('employee.attendances.clock-in');
Route::post('/employee/attendances/clock-out', [\App\Http\Controllers\Employee\EmployeeAttendanceController::class, 'clockOut'])->name('employee.attendances.clock-out');
Route::post('/employee/attendances/leave', [\App\Http\Controllers\Employee\EmployeeAttendanceController::class, 'submitLeave'])->name('employee.attendances.leave');
Route::post('/employee/attendances/dispensation', [\App\Http\Controllers\Employee\EmployeeAttendanceController::class, 'submitDispensation'])->name('employee.attendances.dispensation');
});
require __DIR__.'/settings.php';