From a3ad24e029ace80b1fdb04824d59d06ff380207c Mon Sep 17 00:00:00 2001 From: IlhamIslamy Date: Mon, 27 Apr 2026 22:45:22 +0700 Subject: [PATCH] feat: implement attendance tracking system with admin and employee dashboards --- .../Admin/AttendanceController.php | 45 +++ .../Employee/EmployeeAttendanceController.php | 153 +++++++ app/Http/Middleware/HandleInertiaRequests.php | 4 + app/Models/Attendance.php | 30 ++ ..._04_27_000001_create_attendances_table.php | 31 ++ database/seeders/EmployeeSeeder.php | 42 +- package-lock.json | 23 +- resources/js/components/app-sidebar.tsx | 14 +- resources/js/components/ui/tabs.tsx | 89 +++++ resources/js/components/ui/textarea.tsx | 18 + .../js/pages/admin/attendances/index.tsx | 101 +++++ .../js/pages/employee/attendances/index.tsx | 378 ++++++++++++++++++ resources/js/pages/employee/index.tsx | 14 +- routes/web.php | 9 +- 14 files changed, 925 insertions(+), 26 deletions(-) create mode 100644 app/Http/Controllers/Admin/AttendanceController.php create mode 100644 app/Http/Controllers/Employee/EmployeeAttendanceController.php create mode 100644 app/Models/Attendance.php create mode 100644 database/migrations/2026_04_27_000001_create_attendances_table.php create mode 100644 resources/js/components/ui/tabs.tsx create mode 100644 resources/js/components/ui/textarea.tsx create mode 100644 resources/js/pages/admin/attendances/index.tsx create mode 100644 resources/js/pages/employee/attendances/index.tsx diff --git a/app/Http/Controllers/Admin/AttendanceController.php b/app/Http/Controllers/Admin/AttendanceController.php new file mode 100644 index 0000000..e798f3f --- /dev/null +++ b/app/Http/Controllers/Admin/AttendanceController.php @@ -0,0 +1,45 @@ +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 + ]); + } +} diff --git a/app/Http/Controllers/Employee/EmployeeAttendanceController.php b/app/Http/Controllers/Employee/EmployeeAttendanceController.php new file mode 100644 index 0000000..5d6d147 --- /dev/null +++ b/app/Http/Controllers/Employee/EmployeeAttendanceController.php @@ -0,0 +1,153 @@ +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.'); + } +} diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index 652ec83..6a05f06 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -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'), + ], ]; } } diff --git a/app/Models/Attendance.php b/app/Models/Attendance.php new file mode 100644 index 0000000..bd28343 --- /dev/null +++ b/app/Models/Attendance.php @@ -0,0 +1,30 @@ +belongsTo(Employee::class); + } +} diff --git a/database/migrations/2026_04_27_000001_create_attendances_table.php b/database/migrations/2026_04_27_000001_create_attendances_table.php new file mode 100644 index 0000000..0ee6805 --- /dev/null +++ b/database/migrations/2026_04_27_000001_create_attendances_table.php @@ -0,0 +1,31 @@ +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'); + } +}; diff --git a/database/seeders/EmployeeSeeder.php b/database/seeders/EmployeeSeeder.php index 566f543..99a9141 100644 --- a/database/seeders/EmployeeSeeder.php +++ b/database/seeders/EmployeeSeeder.php @@ -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'], ], ]; diff --git a/package-lock.json b/package-lock.json index cb02e25..b2346d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" } diff --git a/resources/js/components/app-sidebar.tsx b/resources/js/components/app-sidebar.tsx index 3c18048..0796a99 100644 --- a/resources/js/components/app-sidebar.tsx +++ b/resources/js/components/app-sidebar.tsx @@ -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 ( diff --git a/resources/js/components/ui/tabs.tsx b/resources/js/components/ui/tabs.tsx new file mode 100644 index 0000000..7f2c4f2 --- /dev/null +++ b/resources/js/components/ui/tabs.tsx @@ -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) { + return ( + + ) +} + +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 & + VariantProps) { + return ( + + ) +} + +function TabsTrigger({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function TabsContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants } diff --git a/resources/js/components/ui/textarea.tsx b/resources/js/components/ui/textarea.tsx new file mode 100644 index 0000000..e67d8fe --- /dev/null +++ b/resources/js/components/ui/textarea.tsx @@ -0,0 +1,18 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { + return ( +