From 887526bd185548f0603fbf230b0088866a4d7b82 Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Wed, 29 Jul 2026 22:26:24 +0700 Subject: [PATCH] feat: add employee management features including create, edit, and list functionalities - Implemented employee creation and editing forms with validation. - Added employee listing with actions for edit, delete, and reset password. - Integrated toggle for employee active status. - Created reusable UI components for forms, alerts, and data tables. - Updated routing to include employee management endpoints. --- .../Admin/SDM/EmployeeController.php | 119 ++++++++ .../Requests/Admin/SDM/EmployeeRequest.php | 61 ++++ app/Services/Admin/SDM/EmployeeService.php | 104 +++++++ package.json | 4 + pnpm-lock.yaml | 78 ++++++ resources/js/components/app-sidebar.tsx | 3 +- resources/js/components/ui/alert.tsx | 20 +- resources/js/components/ui/radio-group.tsx | 43 +++ resources/js/components/ui/select.tsx | 55 ++-- resources/js/components/ui/switch.tsx | 33 +++ .../js/pages/admin/sdm/employee/columns.tsx | 254 +++++++++++++++++ .../js/pages/admin/sdm/employee/create.tsx | 237 ++++++++++++++++ .../js/pages/admin/sdm/employee/edit.tsx | 264 ++++++++++++++++++ .../js/pages/admin/sdm/employee/index.tsx | 117 ++++++++ routes/web.php | 5 + 15 files changed, 1350 insertions(+), 47 deletions(-) create mode 100644 app/Http/Controllers/Admin/SDM/EmployeeController.php create mode 100644 app/Http/Requests/Admin/SDM/EmployeeRequest.php create mode 100644 app/Services/Admin/SDM/EmployeeService.php create mode 100644 resources/js/components/ui/radio-group.tsx create mode 100644 resources/js/components/ui/switch.tsx create mode 100644 resources/js/pages/admin/sdm/employee/columns.tsx create mode 100644 resources/js/pages/admin/sdm/employee/create.tsx create mode 100644 resources/js/pages/admin/sdm/employee/edit.tsx create mode 100644 resources/js/pages/admin/sdm/employee/index.tsx diff --git a/app/Http/Controllers/Admin/SDM/EmployeeController.php b/app/Http/Controllers/Admin/SDM/EmployeeController.php new file mode 100644 index 0000000..e3ce296 --- /dev/null +++ b/app/Http/Controllers/Admin/SDM/EmployeeController.php @@ -0,0 +1,119 @@ + $this->service->getAll(), + ]); + } + + public function create(): Response + { + return Inertia::render('admin/sdm/employee/create'); + } + + public function store(EmployeeRequest $request): RedirectResponse + { + try { + $this->service->create($request->validated()); + + Inertia::flash('toast', ['type' => 'success', 'message' => 'Pegawai berhasil ditambahkan.']); + } catch (ValidationException $e) { + $firstError = collect($e->errors())->flatten()->first(); + Inertia::flash('toast', ['type' => 'error', 'message' => $firstError ?? 'Terjadi kesalahan.']); + } catch (\Exception $e) { + Inertia::flash('toast', ['type' => 'error', 'message' => $e->getMessage()]); + } + + return to_route('admin.sdm.employees.index'); + } + + public function edit(User $employee): Response + { + return Inertia::render('admin/sdm/employee/edit', [ + 'employee' => $this->service->getById($employee->id), + ]); + } + + public function update(EmployeeRequest $request, User $employee): RedirectResponse + { + try { + $this->service->update($employee, $request->validated()); + + Inertia::flash('toast', ['type' => 'success', 'message' => 'Pegawai berhasil diperbarui.']); + } catch (ValidationException $e) { + $firstError = collect($e->errors())->flatten()->first(); + Inertia::flash('toast', ['type' => 'error', 'message' => $firstError ?? 'Terjadi kesalahan.']); + } catch (\Exception $e) { + Inertia::flash('toast', ['type' => 'error', 'message' => $e->getMessage()]); + } + + return to_route('admin.sdm.employees.index'); + } + + public function destroy(User $employee): RedirectResponse + { + try { + $this->service->delete($employee); + + Inertia::flash('toast', ['type' => 'success', 'message' => 'Pegawai berhasil dihapus.']); + } catch (ValidationException $e) { + $firstError = collect($e->errors())->flatten()->first(); + Inertia::flash('toast', ['type' => 'error', 'message' => $firstError ?? 'Terjadi kesalahan.']); + } catch (\Exception $e) { + Inertia::flash('toast', ['type' => 'error', 'message' => $e->getMessage()]); + } + + return to_route('admin.sdm.employees.index'); + } + + public function toggleActive(User $employee): RedirectResponse + { + try { + $this->service->toggleActive($employee); + + $status = $employee->fresh()->is_active ? 'diaktifkan' : 'dinonaktifkan'; + Inertia::flash('toast', ['type' => 'success', 'message' => "Pegawai berhasil {$status}."]); + } catch (ValidationException $e) { + $firstError = collect($e->errors())->flatten()->first(); + Inertia::flash('toast', ['type' => 'error', 'message' => $firstError ?? 'Terjadi kesalahan.']); + } catch (\Exception $e) { + Inertia::flash('toast', ['type' => 'error', 'message' => $e->getMessage()]); + } + + return to_route('admin.sdm.employees.index'); + } + + public function resetPassword(User $employee): RedirectResponse + { + try { + $this->service->resetPassword($employee); + + Inertia::flash('toast', ['type' => 'success', 'message' => 'Kata sandi pegawai berhasil direset ke kata sandi default.']); + } catch (ValidationException $e) { + $firstError = collect($e->errors())->flatten()->first(); + Inertia::flash('toast', ['type' => 'error', 'message' => $firstError ?? 'Terjadi kesalahan.']); + } catch (\Exception $e) { + Inertia::flash('toast', ['type' => 'error', 'message' => $e->getMessage()]); + } + + return to_route('admin.sdm.employees.index'); + } +} diff --git a/app/Http/Requests/Admin/SDM/EmployeeRequest.php b/app/Http/Requests/Admin/SDM/EmployeeRequest.php new file mode 100644 index 0000000..e70e150 --- /dev/null +++ b/app/Http/Requests/Admin/SDM/EmployeeRequest.php @@ -0,0 +1,61 @@ +route('employee')?->id; + + return [ + 'email' => [ + 'required', + 'email', + 'max:100', + Rule::unique('users', 'email')->ignore($userId, 'id'), + ], + 'username' => [ + 'required', + 'string', + 'max:20', + 'alpha_dash', + Rule::unique('users', 'username')->ignore($userId, 'id'), + ], + 'full_name' => ['required', 'string', 'max:200'], + 'phone_number' => ['nullable', 'string', 'max:20'], + 'gender' => ['nullable', 'in:male,female'], + 'birth_date' => ['nullable', 'date'], + 'address' => ['nullable', 'string'], + 'join_date' => ['required', 'date'], + 'resign_date' => ['nullable', 'date', 'after_or_equal:join_date'], + 'employment_status' => ['required', Rule::in(['full_time', 'part_time', 'contract', 'internship', 'resigned'])], + 'base_salary' => ['required', 'integer', 'min:0'], + ]; + } + + public function attributes(): array + { + return [ + 'email' => 'Email', + 'username' => 'Username', + 'full_name' => 'Nama Lengkap', + 'phone_number' => 'Nomor HP', + 'gender' => 'Jenis Kelamin', + 'birth_date' => 'Tanggal Lahir', + 'address' => 'Alamat', + 'join_date' => 'Tanggal Masuk', + 'resign_date' => 'Tanggal Keluar', + 'employment_status' => 'Status Kepegawaian', + 'base_salary' => 'Gaji Pokok', + ]; + } +} diff --git a/app/Services/Admin/SDM/EmployeeService.php b/app/Services/Admin/SDM/EmployeeService.php new file mode 100644 index 0000000..dc6f619 --- /dev/null +++ b/app/Services/Admin/SDM/EmployeeService.php @@ -0,0 +1,104 @@ +with(['userProfile', 'employee']) + ->latest() + ->get(); + } + + public function getById(int $id): User + { + return User::with(['userProfile', 'employee'])->findOrFail($id); + } + + public function create(array $data): User + { + return DB::transaction(function () use ($data) { + $user = User::create([ + 'email' => $data['email'], + 'username' => $data['username'], + 'password' => config('auth.password_default'), + 'is_active' => true, + ]); + + $user->userProfile()->create([ + 'full_name' => $data['full_name'], + 'phone_number' => $data['phone_number'] ?? null, + 'gender' => $data['gender'] ?? null, + 'birth_date' => $data['birth_date'] ?? null, + 'address' => $data['address'] ?? null, + ]); + + $user->employee()->create([ + 'join_date' => $data['join_date'], + 'resign_date' => $data['resign_date'] ?? null, + 'employment_status' => $data['employment_status'], + 'base_salary' => $data['base_salary'], + ]); + + return $user; + }); + } + + public function update(User $user, array $data): User + { + DB::transaction(function () use ($user, $data) { + $user->update([ + 'email' => $data['email'], + 'username' => $data['username'], + ]); + + $user->userProfile()->updateOrCreate([], [ + 'full_name' => $data['full_name'], + 'phone_number' => $data['phone_number'] ?? null, + 'gender' => $data['gender'] ?? null, + 'birth_date' => $data['birth_date'] ?? null, + 'address' => $data['address'] ?? null, + ]); + + $user->employee()->updateOrCreate([], [ + 'join_date' => $data['join_date'], + 'resign_date' => $data['resign_date'] ?? null, + 'employment_status' => $data['employment_status'], + 'base_salary' => $data['base_salary'], + ]); + }); + + return $user->fresh(['userProfile', 'employee']); + } + + public function delete(User $user): bool + { + return DB::transaction(function () use ($user) { + $user->employee()->delete(); + $user->userProfile()->delete(); + + return $user->delete(); + }); + } + + public function toggleActive(User $user): User + { + $user->update(['is_active' => ! $user->is_active]); + + return $user; + } + + public function resetPassword(User $user): User + { + $defaultPassword = config('auth.password_default', 'Minimal8@'); + $user->update(['password' => $defaultPassword]); + + return $user; + } +} diff --git a/package.json b/package.json index 8929253..3fd6cfd 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,9 @@ "typescript-eslint": "^8.23.0" }, "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@fontsource-variable/inter": "^5.3.0", "@inertiajs/react": "^3.0.0", "@inertiajs/vite": "^3.0.0", @@ -47,6 +50,7 @@ "@radix-ui/react-toggle-group": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.8", "@tailwindcss/vite": "^4.1.11", + "@tanstack/react-table": "^8.21.3", "@types/react": "^19.2.0", "@types/react-dom": "^19.2.0", "@vitejs/plugin-react": "^5.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ee22c54..89adcbf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,15 @@ importers: .: dependencies: + '@dnd-kit/core': + specifier: ^6.3.1 + version: 6.3.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@dnd-kit/sortable': + specifier: ^10.0.0 + version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8) + '@dnd-kit/utilities': + specifier: ^3.2.2 + version: 3.2.2(react@19.2.8) '@fontsource-variable/inter': specifier: ^5.3.0 version: 5.3.0 @@ -62,6 +71,9 @@ importers: '@tailwindcss/vite': specifier: ^4.1.11 version: 4.3.3(vite@8.1.5(@types/node@22.20.1)(jiti@2.7.0)) + '@tanstack/react-table': + specifier: ^8.21.3 + version: 8.21.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@types/react': specifier: ^19.2.0 version: 19.2.17 @@ -340,6 +352,28 @@ packages: '@date-fns/tz@1.5.0': resolution: {integrity: sha512-lwYN/vDPeNRULcepoE/LO2Pgx+7/RV+S9ARfbc9lr2DtGkOD7pAiruHvbR1RX3Qyf6ja47EWJDMsNK5vK08DJg==} + '@dnd-kit/accessibility@3.1.1': + resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==} + peerDependencies: + react: '>=16.8.0' + + '@dnd-kit/core@6.3.1': + resolution: {integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@dnd-kit/sortable@10.0.0': + resolution: {integrity: sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==} + peerDependencies: + '@dnd-kit/core': ^6.3.0 + react: '>=16.8.0' + + '@dnd-kit/utilities@3.2.2': + resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==} + peerDependencies: + react: '>=16.8.0' + '@dotenvx/dotenvx@1.75.1': resolution: {integrity: sha512-/BITOC9dmS/edY2zQwZNicQ059O6RKabtQfyEafV0nGtfYRNHYy1DIPiYVcov40+tob9hfmBnbR963dS+EQ1DQ==} hasBin: true @@ -1447,6 +1481,17 @@ packages: peerDependencies: vite: ^5.2.0 || ^6 || ^7 || ^8 + '@tanstack/react-table@8.21.3': + resolution: {integrity: sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==} + engines: {node: '>=12'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + + '@tanstack/table-core@8.21.3': + resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} + engines: {node: '>=12'} + '@ts-morph/common@0.27.0': resolution: {integrity: sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==} @@ -4185,6 +4230,31 @@ snapshots: '@date-fns/tz@1.5.0': {} + '@dnd-kit/accessibility@3.1.1(react@19.2.8)': + dependencies: + react: 19.2.8 + tslib: 2.8.1 + + '@dnd-kit/core@6.3.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@dnd-kit/accessibility': 3.1.1(react@19.2.8) + '@dnd-kit/utilities': 3.2.2(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + tslib: 2.8.1 + + '@dnd-kit/sortable@10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)': + dependencies: + '@dnd-kit/core': 6.3.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@dnd-kit/utilities': 3.2.2(react@19.2.8) + react: 19.2.8 + tslib: 2.8.1 + + '@dnd-kit/utilities@3.2.2(react@19.2.8)': + dependencies: + react: 19.2.8 + tslib: 2.8.1 + '@dotenvx/dotenvx@1.75.1': dependencies: '@dotenvx/primitives': 0.8.0 @@ -5316,6 +5386,14 @@ snapshots: tailwindcss: 4.3.3 vite: 8.1.5(@types/node@22.20.1)(jiti@2.7.0) + '@tanstack/react-table@8.21.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@tanstack/table-core': 8.21.3 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + + '@tanstack/table-core@8.21.3': {} + '@ts-morph/common@0.27.0': dependencies: fast-glob: 3.3.3 diff --git a/resources/js/components/app-sidebar.tsx b/resources/js/components/app-sidebar.tsx index e23a5b2..1ebd97d 100644 --- a/resources/js/components/app-sidebar.tsx +++ b/resources/js/components/app-sidebar.tsx @@ -41,6 +41,7 @@ import { index as suppliersIndex } from '@/routes/admin/master/suppliers'; import { index as cashAccountsIndex } from '@/routes/admin/finance/cash-accounts'; import { index as expensesIndex } from '@/routes/admin/finance/expenses'; import { index as employeeAdvancesIndex } from '@/routes/admin/finance/employee-advances'; +import { index as employeesIndex } from '@/routes/admin/sdm/employees'; import { index as leaveRequestsIndex } from '@/routes/admin/sdm/leave-requests'; type NavMenuItem = { title: string; href: string; icon: LucideIcon }; @@ -80,7 +81,7 @@ const keuanganItems: NavMenuItem[] = [ ]; const sdmItems: NavMenuItem[] = [ - { title: 'Pegawai', href: '#', icon: UserCircle }, + { title: 'Pegawai', href: employeesIndex.url(), icon: UserCircle }, { title: 'Presensi', href: '#', icon: CalendarCheck }, { title: 'Cuti', href: leaveRequestsIndex.url(), icon: CalendarDays }, ]; diff --git a/resources/js/components/ui/alert.tsx b/resources/js/components/ui/alert.tsx index 78c6538..f99164e 100644 --- a/resources/js/components/ui/alert.tsx +++ b/resources/js/components/ui/alert.tsx @@ -4,13 +4,13 @@ import { cva, type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils" const alertVariants = cva( - "group/alert relative grid w-full gap-0.5 rounded-lg border px-2 py-1.5 text-left text-xs/relaxed has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-1.5 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-3.5", + "relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", { variants: { variant: { default: "bg-card text-card-foreground", destructive: - "bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current", + "bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 [&>svg]:text-current", }, }, defaultVariants: { @@ -39,7 +39,7 @@ function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground", + "col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight", className )} {...props} @@ -55,7 +55,7 @@ function AlertDescription({
) { - return ( -
- ) -} - -export { Alert, AlertTitle, AlertDescription, AlertAction } +export { Alert, AlertTitle, AlertDescription } diff --git a/resources/js/components/ui/radio-group.tsx b/resources/js/components/ui/radio-group.tsx new file mode 100644 index 0000000..20416f0 --- /dev/null +++ b/resources/js/components/ui/radio-group.tsx @@ -0,0 +1,43 @@ +import * as React from "react" +import { CircleIcon } from "lucide-react" +import { RadioGroup as RadioGroupPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function RadioGroup({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function RadioGroupItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + + ) +} + +export { RadioGroup, RadioGroupItem } diff --git a/resources/js/components/ui/select.tsx b/resources/js/components/ui/select.tsx index 41ecc8e..067e9d3 100644 --- a/resources/js/components/ui/select.tsx +++ b/resources/js/components/ui/select.tsx @@ -1,10 +1,8 @@ -"use client" - import * as React from "react" +import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react" import { Select as SelectPrimitive } from "radix-ui" import { cn } from "@/lib/utils" -import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react" function Select({ ...props @@ -13,16 +11,9 @@ function Select({ } function SelectGroup({ - className, ...props }: React.ComponentProps) { - return ( - - ) + return } function SelectValue({ @@ -44,14 +35,14 @@ function SelectTrigger({ data-slot="select-trigger" data-size={size} className={cn( - "flex w-fit items-center justify-between gap-1.5 rounded-md border border-input bg-input/20 px-2 py-1.5 text-xs/relaxed whitespace-nowrap transition-colors outline-none focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/30 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-2 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-7 data-[size=sm]:h-6 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5", + "flex w-fit items-center justify-between gap-2 rounded-md border border-input bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none 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 data-[placeholder]:text-muted-foreground data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground", className )} {...props} > {children} - + ) @@ -68,18 +59,22 @@ function SelectContent({ {children} @@ -112,14 +107,17 @@ function SelectItem({ - + - + {children} @@ -134,10 +132,7 @@ function SelectSeparator({ return ( ) @@ -151,13 +146,12 @@ function SelectScrollUpButton({ - + ) } @@ -170,13 +164,12 @@ function SelectScrollDownButton({ - + ) } diff --git a/resources/js/components/ui/switch.tsx b/resources/js/components/ui/switch.tsx new file mode 100644 index 0000000..dd2a617 --- /dev/null +++ b/resources/js/components/ui/switch.tsx @@ -0,0 +1,33 @@ +import * as React from "react" +import { Switch as SwitchPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function Switch({ + className, + size = "default", + ...props +}: React.ComponentProps & { + size?: "sm" | "default" +}) { + return ( + + + + ) +} + +export { Switch } diff --git a/resources/js/pages/admin/sdm/employee/columns.tsx b/resources/js/pages/admin/sdm/employee/columns.tsx new file mode 100644 index 0000000..517a2fc --- /dev/null +++ b/resources/js/pages/admin/sdm/employee/columns.tsx @@ -0,0 +1,254 @@ +import type { ColumnDef } from '@tanstack/react-table'; +import { router } from '@inertiajs/react'; +import { ArrowUpDown, KeyRound, Pencil, Trash2 } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Switch } from '@/components/ui/switch'; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@/components/ui/tooltip'; + +export type Employee = { + id: number; + email: string; + username: string; + is_active: boolean; + user_profile: { + full_name: string; + phone_number: string | null; + } | null; + employee: { + join_date: string; + employment_status: string; + base_salary: number; + } | null; +}; + +function getEmploymentStatusLabel(status: string): string { + const labels: Record = { + full_time: 'Full Time', + part_time: 'Part Time', + contract: 'Kontrak', + internship: 'Magang', + resigned: 'Keluar', + }; + + return labels[status] ?? status; +} + +function formatCurrency(amount: number): string { + return new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0, + }).format(amount); +} + +type CreateColumnsParams = { + handleEdit: (employee: Employee) => void; + handleDeleteClick: (employee: Employee) => void; + handleResetPassword: (employee: Employee) => void; + toggleActiveUrl: (id: number) => string; +}; + +export function createEmployeeColumns( + params: CreateColumnsParams, +): ColumnDef[] { + const { handleEdit, handleDeleteClick, handleResetPassword, toggleActiveUrl } = params; + + return [ + { + id: 'no', + header: () => No, + cell: ({ row }) => ( + + {row.index + 1} + + ), + meta: { + className: 'w-[50px] text-center', + headerClassName: 'w-[50px] text-center', + }, + }, + { + accessorKey: 'user_profile.full_name', + id: 'full_name', + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const employee = row.original; + + return ( +
+ + {employee.user_profile?.full_name ?? '-'} + + + {employee.email} + +
+ ); + }, + }, + { + accessorKey: 'username', + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + + {row.getValue('username') as string} + + ), + }, + { + accessorKey: 'user_profile.phone_number', + id: 'phone_number', + header: () => No. HP, + cell: ({ row }) => { + const employee = row.original; + + return {employee.user_profile?.phone_number ?? '-'}; + }, + }, + { + accessorKey: 'employee.employment_status', + id: 'employment_status', + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const employee = row.original; + + return ( + + {getEmploymentStatusLabel(employee.employee?.employment_status ?? '')} + + ); + }, + }, + { + accessorKey: 'is_active', + header: () => Aktif, + meta: { + className: 'w-[80px] text-center', + headerClassName: 'w-[80px] text-center', + }, + cell: ({ row }) => { + const employee = row.original; + + return ( +
+ { + router.post(toggleActiveUrl(employee.id), {}, { + preserveScroll: true, + }); + }} + /> +
+ ); + }, + }, + { + id: 'actions', + header: () => Aksi, + meta: { + className: 'w-[120px] text-center', + headerClassName: 'w-[120px] text-center', + }, + cell: ({ row }) => { + const employee = row.original; + + return ( + +
+ + + + + + Edit + + + + + + + + + Reset Kata Sandi + + + + + + + + + Hapus + + +
+
+ ); + }, + }, + ]; +} diff --git a/resources/js/pages/admin/sdm/employee/create.tsx b/resources/js/pages/admin/sdm/employee/create.tsx new file mode 100644 index 0000000..1fe54bf --- /dev/null +++ b/resources/js/pages/admin/sdm/employee/create.tsx @@ -0,0 +1,237 @@ +import { DatePicker } from '@/components/date-picker'; +import InputError from '@/components/input-error'; +import { RupiahInput } from '@/components/rupiah-input'; +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { Textarea } from '@/components/ui/textarea'; +import { index as employeeIndex, store } from '@/routes/admin/sdm/employees'; +import { Form, Head } from '@inertiajs/react'; +import { AlertCircle, ArrowLeft } from 'lucide-react'; +import { useState } from 'react'; + +export default function EmployeeCreate() { + const [joinDate, setJoinDate] = useState(undefined); + const [resignDate, setResignDate] = useState(undefined); + + return ( + <> + + +
+
+
+

+ Tambah Pegawai +

+
+ +
+ + + + Informasi Kata Sandi + + Kata sandi default untuk akun baru adalah Minimal8@ + + + +
+ {({ errors, processing }) => ( + <> +
+ + + Akun + + +
+ + + +
+
+ + + +
+
+
+ + + + Profil + + +
+ + + +
+
+ + + +
+
+ + +
+ + +
+
+ + +
+
+ +
+
+ + {}} + placeholder="Pilih tanggal lahir" + /> + +
+
+ +