From a4a22e1cc5511d3d26c48639ec457a337b50724d Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Fri, 17 Apr 2026 14:54:21 +0700 Subject: [PATCH] feat: implement user management module with CRUD operations, including user creation, editing, and deletion, along with profile handling --- .../Admin/Master/UserController.php | 110 +++++++ app/Http/Middleware/HandleInertiaRequests.php | 2 +- .../Requests/Admin/Master/UserRequest.php | 41 +++ app/Models/User.php | 34 ++- app/Models/UserProfile.php | 20 ++ config/auth.php | 1 + database/factories/UserFactory.php | 37 +-- .../0001_01_01_000000_create_users_table.php | 11 +- ...4_17_132236_create_user_profiles_table.php | 36 +++ database/seeders/UserSeeder.php | 13 +- resources/js/components/app-sidebar.tsx | 8 +- .../js/pages/admin/master/user/create.tsx | 223 ++++++++++++++ resources/js/pages/admin/master/user/edit.tsx | 224 ++++++++++++++ .../js/pages/admin/master/user/index.tsx | 273 ++++++++++++++++++ resources/js/types/auth.ts | 17 +- routes/master.php | 10 + 16 files changed, 1013 insertions(+), 47 deletions(-) create mode 100644 app/Http/Controllers/Admin/Master/UserController.php create mode 100644 app/Http/Requests/Admin/Master/UserRequest.php create mode 100644 app/Models/UserProfile.php create mode 100644 database/migrations/2026_04_17_132236_create_user_profiles_table.php create mode 100644 resources/js/pages/admin/master/user/create.tsx create mode 100644 resources/js/pages/admin/master/user/edit.tsx create mode 100644 resources/js/pages/admin/master/user/index.tsx diff --git a/app/Http/Controllers/Admin/Master/UserController.php b/app/Http/Controllers/Admin/Master/UserController.php new file mode 100644 index 0000000..a276900 --- /dev/null +++ b/app/Http/Controllers/Admin/Master/UserController.php @@ -0,0 +1,110 @@ + User::with('profile')->latest()->get(), + 'defaultPassword' => config('auth.password_default', 'password'), + ]); + } + + public function create(): Response + { + return Inertia::render('admin/master/user/create'); + } + + public function store(UserRequest $request): RedirectResponse + { + $validated = $request->validated(); + + DB::transaction(function () use ($validated) { + $user = User::create([ + 'username' => $validated['username'], + 'email' => $validated['email'], + 'password' => Hash::make(config('auth.password_default')), + ]); + + $user->profile()->create([ + 'nik' => $validated['nik'], + 'full_name' => $validated['full_name'], + 'phone_number' => $validated['phone_number'], + 'address' => $validated['address'], + 'birth_place' => $validated['birth_place'], + 'birth_date' => $validated['birth_date'], + ]); + }); + + return redirect()->route('user.index')->with('success', 'Data berhasil disimpan'); + } + + public function edit(User $user): Response + { + $user->load('profile'); + + return Inertia::render('admin/master/user/edit', [ + 'user' => $user, + ]); + } + + public function update(UserRequest $request, User $user): RedirectResponse + { + $validated = $request->validated(); + + DB::transaction(function () use ($user, $validated) { + $user->update([ + 'username' => $validated['username'], + 'email' => $validated['email'], + ]); + + $user->profile()->update([ + 'nik' => $validated['nik'], + 'full_name' => $validated['full_name'], + 'phone_number' => $validated['phone_number'], + 'address' => $validated['address'], + 'birth_place' => $validated['birth_place'], + 'birth_date' => $validated['birth_date'], + ]); + }); + + return redirect()->route('user.index')->with('success', 'Data berhasil diperbarui'); + } + + public function resetPassword(User $user): RedirectResponse + { + $user->update([ + 'password' => Hash::make(config('auth.password_default', 'password')), + ]); + + return redirect()->back()->with('success', 'Password berhasil direset ke default'); + } + + public function destroy(User $user): RedirectResponse + { + $user->delete(); + + return redirect()->back()->with('success', 'Data berhasil dihapus'); + } + + public function bulkDestroy(Request $request): RedirectResponse + { + $ids = $request->input('ids'); + + User::whereIn('id', $ids)->delete(); + + return redirect()->back()->with('success', 'Data terpilih berhasil dihapus'); + } +} diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index 25f7be3..9ffc373 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -39,7 +39,7 @@ public function share(Request $request): array ...parent::share($request), 'name' => config('app.name'), 'auth' => [ - 'user' => $request->user(), + 'user' => $request->user()?->load('profile'), ], 'sidebarOpen' => ! $request->hasCookie('sidebar_state') || $request->cookie('sidebar_state') === 'true', 'flash' => [ diff --git a/app/Http/Requests/Admin/Master/UserRequest.php b/app/Http/Requests/Admin/Master/UserRequest.php new file mode 100644 index 0000000..29b22fc --- /dev/null +++ b/app/Http/Requests/Admin/Master/UserRequest.php @@ -0,0 +1,41 @@ +route('user')?->id; + + return [ + 'username' => [ + 'required', + 'string', + 'max:20', + Rule::unique('users', 'username')->ignore($userId), + ], + 'email' => [ + 'required', + 'string', + 'email', + 'max:100', + Rule::unique('users', 'email')->ignore($userId), + ], + 'nik' => ['required', 'max:16', Rule::unique('user_profiles', 'nik')->ignore($userId)], + 'full_name' => ['required', 'string', 'max:100'], + 'phone_number' => ['required', 'string', 'max:20'], + 'address' => ['required', 'string'], + 'birth_place' => ['required', 'string', 'max:100'], + 'birth_date' => ['required', 'date'], + ]; + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 5e1baf1..95b9f7d 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -3,38 +3,46 @@ namespace App\Models; // use Illuminate\Contracts\Auth\MustVerifyEmail; -use Database\Factories\UserFactory; -use Illuminate\Database\Eloquent\Attributes\Fillable; +use Illuminate\Database\Eloquent\Attributes\Guarded; use Illuminate\Database\Eloquent\Attributes\Hidden; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\HasOne; +use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Laravel\Fortify\TwoFactorAuthenticatable; -#[Fillable(['name', 'email', 'password'])] -#[Hidden(['password', 'two_factor_secret', 'two_factor_recovery_codes', 'remember_token'])] +#[Guarded('id')] +#[Hidden(['password'])] class User extends Authenticatable { - /** @use HasFactory */ - use HasFactory, Notifiable, TwoFactorAuthenticatable; + use HasFactory, Notifiable, SoftDeletes, TwoFactorAuthenticatable; + + protected $appends = ['name']; - /** - * Get the attributes that should be cast. - * - * @return array - */ protected function casts(): array { return [ - 'email_verified_at' => 'datetime', 'password' => 'hashed', - 'two_factor_confirmed_at' => 'datetime', ]; } + protected function name(): Attribute + { + return Attribute::make( + get: fn () => $this->profile?->full_name ?? $this->username + ); + } + public function expenses(): HasMany { return $this->hasMany(Expense::class); } + + public function profile(): HasOne + { + return $this->hasOne(UserProfile::class); + } } diff --git a/app/Models/UserProfile.php b/app/Models/UserProfile.php new file mode 100644 index 0000000..86cecca --- /dev/null +++ b/app/Models/UserProfile.php @@ -0,0 +1,20 @@ +belongsTo(User::class); + } +} diff --git a/config/auth.php b/config/auth.php index d7568ff..34c85b0 100644 --- a/config/auth.php +++ b/config/auth.php @@ -114,4 +114,5 @@ 'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800), + 'password_default' => env('AUTH_PASSWORD_DEFAULT', 'Minimal8@'), ]; diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index 98825c8..c3bbf92 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -5,7 +5,6 @@ use App\Models\User; use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Support\Facades\Hash; -use Illuminate\Support\Str; /** * @extends Factory @@ -25,36 +24,26 @@ class UserFactory extends Factory public function definition(): array { return [ - 'name' => fake()->name(), + 'username' => fake()->userName(), 'email' => fake()->unique()->safeEmail(), - 'email_verified_at' => now(), 'password' => static::$password ??= Hash::make('password'), - 'remember_token' => Str::random(10), - 'two_factor_secret' => null, - 'two_factor_recovery_codes' => null, - 'two_factor_confirmed_at' => null, ]; } /** - * Indicate that the model's email address should be unverified. + * Configure the model factory. */ - public function unverified(): static + public function configure(): static { - return $this->state(fn (array $attributes) => [ - 'email_verified_at' => null, - ]); - } - - /** - * Indicate that the model has two-factor authentication configured. - */ - public function withTwoFactor(): static - { - return $this->state(fn (array $attributes) => [ - 'two_factor_secret' => encrypt('secret'), - 'two_factor_recovery_codes' => encrypt(json_encode(['recovery-code-1'])), - 'two_factor_confirmed_at' => now(), - ]); + return $this->afterCreating(function (User $user) { + $user->profile()->create([ + 'nik' => fake()->numerify('################'), + 'full_name' => fake()->name(), + 'phone_number' => fake()->phoneNumber(), + 'address' => fake()->address(), + 'birth_place' => fake()->city(), + 'birth_date' => fake()->date(), + ]); + }); } } diff --git a/database/migrations/0001_01_01_000000_create_users_table.php b/database/migrations/0001_01_01_000000_create_users_table.php index 05fb5d9..45bc592 100644 --- a/database/migrations/0001_01_01_000000_create_users_table.php +++ b/database/migrations/0001_01_01_000000_create_users_table.php @@ -13,12 +13,13 @@ public function up(): void { Schema::create('users', function (Blueprint $table) { $table->id(); - $table->string('name'); - $table->string('email')->unique(); - $table->timestamp('email_verified_at')->nullable(); - $table->string('password'); + $table->string('username', 20); + $table->string('email', 100)->unique(); + $table->string('password', 60); $table->rememberToken(); - $table->timestamps(); + $table->timestamp('created_at')->useCurrent(); + $table->timestamp('updated_at')->nullable()->useCurrentOnUpdate(); + $table->softDeletes(); }); Schema::create('password_reset_tokens', function (Blueprint $table) { diff --git a/database/migrations/2026_04_17_132236_create_user_profiles_table.php b/database/migrations/2026_04_17_132236_create_user_profiles_table.php new file mode 100644 index 0000000..4508ea3 --- /dev/null +++ b/database/migrations/2026_04_17_132236_create_user_profiles_table.php @@ -0,0 +1,36 @@ +id(); + $table->foreignId('user_id')->constrained()->cascadeOnDelete(); + $table->string('nik', 16); + $table->string('full_name', 100); + $table->string('phone_number', 20); + $table->text('address'); + $table->string('birth_place', 100); + $table->date('birth_date'); + $table->timestamp('created_at')->useCurrent(); + $table->timestamp('updated_at')->nullable()->useCurrentOnUpdate(); + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('user_profiles'); + } +}; diff --git a/database/seeders/UserSeeder.php b/database/seeders/UserSeeder.php index 0e5f961..4ef62ef 100644 --- a/database/seeders/UserSeeder.php +++ b/database/seeders/UserSeeder.php @@ -13,10 +13,19 @@ class UserSeeder extends Seeder */ public function run(): void { - User::create([ - 'name' => 'Yoga Pangestu', + $user = User::create([ + 'username' => 'pangestu', 'email' => 'info.pangestuyoga@gmail.com', 'password' => Hash::make('Minimal8@'), ]); + + $user->profile()->create([ + 'nik' => '3213051307900001', + 'full_name' => 'Yoga Pangestu', + 'phone_number' => '082121495806', + 'address' => 'Jl. Contoh No. 123', + 'birth_place' => 'Jakarta', + 'birth_date' => '1990-01-01', + ]); } } diff --git a/resources/js/components/app-sidebar.tsx b/resources/js/components/app-sidebar.tsx index 01da09e..f5f3bf8 100644 --- a/resources/js/components/app-sidebar.tsx +++ b/resources/js/components/app-sidebar.tsx @@ -1,5 +1,5 @@ import { Link } from '@inertiajs/react'; -import { Boxes, LayoutGrid, List, Wallet } from 'lucide-react'; +import { Boxes, LayoutGrid, List, User, Wallet } from 'lucide-react'; import AppLogo from '@/components/app-logo'; import { NavMain } from '@/components/nav-main'; import { @@ -16,6 +16,7 @@ import category from '@/routes/category'; import type { NavItem } from '@/types'; import product from '@/routes/product'; import expense from '@/routes/expense'; +import user from '@/routes/user'; const mainNavItems: NavItem[] = [ { @@ -26,6 +27,11 @@ const mainNavItems: NavItem[] = [ ]; const masterNavItems: NavItem[] = [ + { + title: 'Pegawai', + href: user.index().url, + icon: User, + }, { title: 'Kategori', href: category.index().url, diff --git a/resources/js/pages/admin/master/user/create.tsx b/resources/js/pages/admin/master/user/create.tsx new file mode 100644 index 0000000..1e4697e --- /dev/null +++ b/resources/js/pages/admin/master/user/create.tsx @@ -0,0 +1,223 @@ +import { Head, Link } from '@inertiajs/react'; +import { useForm } from '@inertiajs/react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Field } from "@/components/ui/field" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import userRoutes from '@/routes/user'; +import React from 'react'; +import { toast } from 'sonner'; +import { Textarea } from '@/components/ui/textarea'; +import { Calendar } from "@/components/ui/calendar" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" + +export default function UserCreate() { + const [isCalendarOpen, setIsCalendarOpen] = React.useState(false); + + const { data, setData, post, processing, errors } = useForm({ + username: '', + email: '', + nik: '', + full_name: '', + phone_number: '', + address: '', + birth_place: '', + birth_date: '', + }); + + const onSubmit = (e: React.FormEvent) => { + e.preventDefault(); + post(userRoutes.store().url, { + onSuccess: (response: any) => { + toast.success(response.props.flash.success); + }, + }); + }; + + return ( +
+ + +
+
+

Tambah Pegawai

+
+ + + +
+ +
+
+
+ + + Informasi Profil + + + + + setData('full_name', e.target.value)} + autoComplete='off' + placeholder='Contoh: John Doe' + /> + {errors.full_name &&

{errors.full_name}

} +
+ +
+ + + setData('nik', e.target.value)} + autoComplete='off' + placeholder='Contoh: 3213051307900001' + maxLength={16} + /> + {errors.nik &&

{errors.nik}

} +
+ + + + setData('phone_number', e.target.value)} + autoComplete='off' + placeholder='Contoh: 08123456789' + /> + {errors.phone_number &&

{errors.phone_number}

} +
+ + + + setData('birth_place', e.target.value)} + autoComplete='off' + placeholder='Contoh: Jakarta' + /> + {errors.birth_place &&

{errors.birth_place}

} +
+ + + + + + + + + { + if (selectedDate) { + setData('birth_date', selectedDate.getFullYear() + "-" + String(selectedDate.getMonth() + 1).padStart(2, '0') + "-" + String(selectedDate.getDate()).padStart(2, '0')); + } else { + setData('birth_date', ''); + } + setIsCalendarOpen(false); + }} + /> + + + {errors.birth_date &&

{errors.birth_date}

} +
+
+ + + +